001// Copyright (C) 1999-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
002// All rights reserved.  Use of this class is limited.
003// Please see the LICENSE for more information.
004
005package com.oreilly.servlet.multipart;
006
007import java.io.File;
008import java.io.InputStream;
009import java.io.OutputStream;
010import java.io.BufferedOutputStream;
011import java.io.FileOutputStream;
012import java.io.IOException;
013import javax.servlet.ServletInputStream;
014
015/**
016 * A <code>FilePart</code> is an upload part which represents a 
017 * <code>INPUT TYPE="file"</code> form parameter.  Note that because file 
018 * upload data arrives via a single InputStream, each FilePart's contents
019 * must be read before moving onto the next part.  Don't try to store a
020 * FilePart object for later processing because by then their content will
021 * have been passed by.
022 * 
023 * @author Geoff Soutter
024 * @version 1.2, 2001/01/22, getFilePath() addition thanks to Stefan Eissing
025 * @version 1.1, 2000/11/26, writeTo() bug fix thanks to Mike Shivas
026 * @version 1.0, 2000/10/27, initial revision
027 */
028public class FilePart extends Part {
029  
030  /** "file system" name of the file  */
031  private String fileName;     
032  
033  /** path of the file as sent in the request, if given */
034  private String filePath;
035
036  /** content type of the file */
037  private String contentType;   
038  
039  /** input stream containing file data */
040  private PartInputStream partInput;  
041    
042  /** file rename policy */
043  private FileRenamePolicy policy;
044
045  /**
046   * Construct a file part; this is called by the parser.
047   * 
048   * @param name the name of the parameter.
049   * @param in the servlet input stream to read the file from.
050   * @param boundary the MIME boundary that delimits the end of file.
051   * @param contentType the content type of the file provided in the 
052   * MIME header.
053   * @param fileName the file system name of the file provided in the 
054   * MIME header.
055   * @param filePath the file system path of the file provided in the
056   * MIME header (as specified in disposition info).
057   * 
058   * @exception IOException     if an input or output exception has occurred.
059   */
060  FilePart(String name, ServletInputStream in, String boundary,
061           String contentType, String fileName, String filePath)
062                                                   throws IOException {
063    super(name);
064    this.fileName = fileName;
065    this.filePath = filePath;
066    this.contentType = contentType;
067    partInput = new PartInputStream(in, boundary);
068  }
069
070  /**
071   * Puts in place the specified policy for handling file name collisions.
072   */
073  public void setRenamePolicy(FileRenamePolicy policy) {
074    this.policy = policy;
075  }
076  
077  /**
078   * Returns the name that the file was stored with on the remote system, 
079   * or <code>null</code> if the user didn't enter a file to be uploaded. 
080   * Note: this is not the same as the name of the form parameter used to 
081   * transmit the file; that is available from the <code>getName</code>
082   * method.  Further note: if file rename logic is in effect, the file
083   * name can change during the writeTo() method when there's a collision
084   * with another file of the same name in the same directory.  If this
085   * matters to you, be sure to pay attention to when you call the method.
086   * 
087   * @return name of file uploaded or <code>null</code>.
088   * 
089   * @see Part#getName()
090   */
091  public String getFileName() {
092    return fileName;
093  }
094
095  /**
096   * Returns the full path and name of the file on the remote system,
097   * or <code>null</code> if the user didn't enter a file to be uploaded.
098   * If path information was not supplied by the remote system, this method
099   * will return the same as <code>getFileName()</code>.
100   *
101   * @return path of file uploaded or <code>null</code>.
102   *
103   * @see Part#getName()
104   */
105  public String getFilePath() {
106    return filePath;
107  }
108
109  /** 
110   * Returns the content type of the file data contained within.
111   * 
112   * @return content type of the file data.
113   */
114  public String getContentType() {
115    return contentType;
116  }
117  
118  /**
119   * Returns an input stream which contains the contents of the
120   * file supplied. If the user didn't enter a file to upload
121   * there will be <code>0</code> bytes in the input stream.
122   * It's important to read the contents of the InputStream 
123   * immediately and in full before proceeding to process the 
124   * next part.  The contents will otherwise be lost on moving
125   * to the next part.
126   * 
127   * @return an input stream containing contents of file.
128   */
129  public InputStream getInputStream() {
130    return partInput;
131  }
132
133  /**
134   * Write this file part to a file or directory. If the user 
135   * supplied a file, we write it to that file, and if they supplied
136   * a directory, we write it to that directory with the filename
137   * that accompanied it. If this part doesn't contain a file this
138   * method does nothing.
139   *
140   * @return number of bytes written
141   * @exception IOException     if an input or output exception has occurred.
142   */
143  public long writeTo(File fileOrDirectory) throws IOException {
144    long written = 0;
145    
146    OutputStream fileOut = null;
147    try {
148      // Only do something if this part contains a file
149      if (fileName != null) {
150        // Check if user supplied directory
151        File file;
152        if (fileOrDirectory.isDirectory()) {
153          // Write it to that dir the user supplied, 
154          // with the filename it arrived with
155          file = new File(fileOrDirectory, fileName);
156        }
157        else {
158          // Write it to the file the user supplied,
159          // ignoring the filename it arrived with
160          file = fileOrDirectory;
161        }
162        if (policy != null) {
163          file = policy.rename(file);
164          fileName = file.getName();
165        }
166        fileOut = new BufferedOutputStream(new FileOutputStream(file));
167        written = write(fileOut);
168      }
169    }
170    finally {
171      if (fileOut != null) fileOut.close();
172    }
173    return written;
174  }
175
176  /**
177   * Write this file part to the given output stream. If this part doesn't 
178   * contain a file this method does nothing.
179   *
180   * @return number of bytes written.
181   * @exception IOException     if an input or output exception has occurred.
182   */
183  public long writeTo(OutputStream out) throws IOException {
184    long size=0;
185    // Only do something if this part contains a file
186    if (fileName != null) {
187      // Write it out
188      size = write( out );
189    }
190    return size;
191  }
192
193  /**
194   * Internal method to write this file part; doesn't check to see
195   * if it has contents first.
196   *
197   * @return number of bytes written.
198   * @exception IOException     if an input or output exception has occurred.
199   */
200  long write(OutputStream out) throws IOException {
201    // decode macbinary if this was sent
202    if (contentType.equals("application/x-macbinary")) {
203      out = new MacBinaryDecoderOutputStream(out);
204    }
205    long size=0;
206    int read;
207    byte[] buf = new byte[8 * 1024];
208    while((read = partInput.read(buf)) != -1) {
209      out.write(buf, 0, read);
210      size += read;
211    }
212    return size;
213  }
214  
215  /**
216   * Returns <code>true</code> to indicate this part is a file.
217   * 
218   * @return true.
219   */
220  public boolean isFile() {
221    return true;
222  }
223}