View Javadoc

1   /*
2    * ------------------------------------------------------------------------------
3    * Hermes FTP Server
4    * Copyright (c) 2005-2007 Lars Behnke
5    * ------------------------------------------------------------------------------
6    * 
7    * This file is part of Hermes FTP Server.
8    * 
9    * Hermes FTP Server is free software; you can redistribute it and/or modify
10   * it under the terms of the GNU General Public License as published by
11   * the Free Software Foundation; either version 2 of the License, or
12   * (at your option) any later version.
13   * 
14   * Hermes FTP Server is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17   * GNU General Public License for more details.
18   * 
19   * You should have received a copy of the GNU General Public License
20   * along with Hermes FTP Server; if not, write to the Free Software
21   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22   * ------------------------------------------------------------------------------
23   */
24  
25  package net.sf.hermesftp.cmd;
26  
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.UnsupportedEncodingException;
31  import java.net.Socket;
32  import java.util.HashMap;
33  import java.util.Map;
34  import java.util.zip.InflaterInputStream;
35  
36  import net.sf.hermesftp.exception.FtpCmdException;
37  import net.sf.hermesftp.exception.FtpException;
38  import net.sf.hermesftp.exception.FtpPermissionException;
39  import net.sf.hermesftp.exception.FtpUniqueConstraintException;
40  import net.sf.hermesftp.streams.BlockModeInputStream;
41  import net.sf.hermesftp.streams.RecordInputStream;
42  import net.sf.hermesftp.streams.RecordReadSupport;
43  import net.sf.hermesftp.streams.TextInputStream;
44  import net.sf.hermesftp.utils.TransferRateLimiter;
45  
46  import org.apache.commons.logging.Log;
47  import org.apache.commons.logging.LogFactory;
48  
49  /***
50   * Ancestor command class that is extended by commands that store data on the remote file system.
51   * 
52   * @author Lars Behnke
53   */
54  public abstract class AbstractFtpCmdStor extends AbstractFtpCmd {
55  
56      private static Log          log                 = LogFactory.getLog(AbstractFtpCmdStor.class);
57  
58      private TransferRateLimiter transferRateLimiter = new TransferRateLimiter();
59  
60      private long                fileSize;
61  
62      private long                completed;
63  
64      private boolean             abortRequested;
65  
66      /***
67       * Executes the command. This operation acts as a template method calling primitive operations
68       * implemented by the sub classes.
69       * 
70       * @param unique True, if file that is supposed to be stored may not exist on the remote file
71       *            system.
72       * @throws FtpCmdException Wrapper class for any exception thrown in the command.
73       */
74      public void execute(boolean unique) throws FtpCmdException {
75  
76          /* Get relevant information from context */
77          File file = new File(getPathArg());
78          int mode = getCtx().getTransmissionMode();
79          int struct = getCtx().getStorageStructure();
80          int type = getCtx().getDataType();
81          String charset = type == DT_ASCII || type == DT_EBCDIC ? getCtx().getCharset() : null;
82          long fileOffset = getAndResetFileOffset();
83          getTransferRateLimiter().init(getCtx().getMaxUploadRate());
84  
85          try {
86              /* Check availability and access rights */
87              doPerformAccessChecks(unique, file, fileOffset);
88  
89              /* Initialize restart markers (block transfer mode) */
90              Map<Long, Long> restartMarkers = new HashMap<Long, Long>();
91              getCtx().setAttribute(ATTR_RESTART_MARKERS, restartMarkers);
92  
93              /* Wrap inbound data stream and call handler method */
94              msgOut(MSG150);
95  
96              Socket dataSocket = getCtx().getDataSocketProvider().provideSocket();
97              InputStream dataIn = dataSocket.getInputStream();
98              if (struct == STRUCT_RECORD) {
99                  RecordReadSupport recordIn = createRecInputStream(dataIn, mode, charset, restartMarkers);
100                 doStoreRecordData(recordIn, file, fileOffset);
101             } else if (struct == STRUCT_FILE) {
102                 InputStream fileIn = createInputStream(dataIn, mode, restartMarkers, charset);
103                 doStoreFileData(fileIn, file, fileOffset);
104             } else {
105                 log.error("Unknown data type");
106                 msgOut(MSG550, "Unsupported data type");
107             }
108             // TODO delegate event to FtpEventListener
109             // getCtx().getEventListener().
110 
111         } catch (FtpUniqueConstraintException e) {
112             msgOut(MSG553);
113         } catch (FtpPermissionException e) {
114             msgOut(MSG550_PERM);
115         } catch (FtpException e) {
116             msgOut(MSG550_MSG, e.getMessage());
117             log.warn(e.getMessage());
118         } catch (UnsupportedEncodingException e) {
119             msgOut(MSG550_MSG, "Unsupported Encoding: " + charset);
120             log.error(e.toString());
121         } catch (IOException e) {
122             msgOut(MSG550);
123             log.error(e.toString());
124         } catch (RuntimeException e) {
125             msgOut(MSG550);
126             log.error(e.toString());
127         } finally {
128             getCtx().closeSockets();
129         }
130     }
131 
132     /***
133      * Creates an input stream that supports unstructured file data.
134      * 
135      * @param is The nested input stream.
136      * @param mode The transmission mode.
137      * @param charset The encoding or null if binary.
138      * @param restartMarkers Optional map that stores restart markers.
139      * @return The stream object.
140      * @throws UnsupportedEncodingException Thrown if encoding is unknown.
141      */
142     private InputStream createInputStream(InputStream is, int mode, Map<Long, Long> restartMarkers,
143                                           String charset) throws UnsupportedEncodingException {
144         InputStream result = null;
145         if (mode == MODE_BLOCK) {
146             byte[] eorBytes = getEorBytes(null);
147             result = new BlockModeInputStream(is, eorBytes, restartMarkers);
148         } else if (mode == MODE_STREAM) {
149             result = is;
150         } else if (mode == MODE_ZIP) {
151             result = new InflaterInputStream(is);
152         } else {
153             log.error("Unsupported file mode: " + mode);
154         }
155         if (charset != null) {
156             result = new TextInputStream(is, charset);
157         }
158         return result;
159 
160     }
161 
162     /***
163      * Creates an input stream that supports reading records.
164      * 
165      * @param is The nested input stream.
166      * @param mode The transmission mode.
167      * @param charset The encoding or null if binary.
168      * @param restartMarkers Optional map that stores restart markers.
169      * @return The stream object.
170      * @throws UnsupportedEncodingException Thrown if encoding unknown.
171      */
172     private RecordReadSupport createRecInputStream(InputStream is, int mode, String charset,
173                                                    Map<Long, Long> restartMarkers)
174             throws UnsupportedEncodingException {
175         RecordReadSupport result = null;
176         byte[] eorBytes = charset == null ? new byte[0] : getEorBytes(charset);
177         if (mode == MODE_BLOCK) {
178             result = new BlockModeInputStream(is, eorBytes, restartMarkers);
179         } else if (mode == MODE_STREAM) {
180             result = new RecordInputStream(is, getEorBytes(charset));
181         } else if (mode == MODE_ZIP) {
182             result = new RecordInputStream(new InflaterInputStream(is), getEorBytes(charset));
183         } else {
184             log.error("Unsupported record mode: " + mode);
185         }
186         if (charset != null) {
187             result = new TextInputStream((InputStream) result, charset);
188         }
189         return result;
190 
191     }
192 
193     /***
194      * Returns the EOR-byte representation in non-record text files, which corresponds to the line
195      * break sequence of the passed character set.
196      * 
197      * @param charset The character set.
198      * @return The EOR marker.
199      */
200     private static byte[] getEorBytes(String charset) {
201         String lineSep = System.getProperty("line.separator");
202         try {
203             if (charset == null) {
204                 return lineSep.getBytes();
205             } else {
206                 return lineSep.getBytes(charset);
207             }
208         } catch (UnsupportedEncodingException e) {
209             log.error(e);
210             return lineSep.getBytes();
211         }
212     }
213 
214     /***
215      * {@inheritDoc}
216      */
217     public boolean handleAsyncCmd(String req) {
218         boolean result;
219         if (req == null || isResponded()) {
220             result = false;
221         } else if (req.toUpperCase().startsWith("STAT")) {
222             String stat = "STAT: " + getCompleted() + " from " + getFileSize() + " completed";
223             log.info(stat);
224             // TODO Return statistics response.
225             result = true;
226         } else if (req.toUpperCase().startsWith("ABOR")) {
227             abortRequested = true;
228             result = true;
229         } else {
230             result = false;
231         }
232         return result;
233     }
234 
235     /***
236      * Checks availability and access rights for the current folder and passed file. The methods
237      * acts as a primitive operation that is called by the template method
238      * <code>execute(boolean)</code>;
239      * 
240      * @param unique True, if destination file may not exist already.
241      * @param file The destination file.
242      * @param offset The file offset (-1 on append).
243      * @throws FtpException Thrown if permission rules have been violated or resource limits have
244      *             been exceeded.
245      */
246     protected abstract void doPerformAccessChecks(boolean unique, File file, long offset) throws FtpException;
247 
248     /***
249      * Stores record based data as file. The method acts as a primitive operation that is called by
250      * the template method <code>execute(boolean)</code>;
251      * 
252      * @param rrs The wrapped input stream.
253      * @param file Destination file.
254      * @param offset The file offset (-1 on append).
255      * @throws IOException Thrown if IO fails or if at least one resource limit was reached.
256      */
257     protected abstract void doStoreRecordData(RecordReadSupport rrs, File file, long offset)
258             throws IOException;
259 
260     /***
261      * Stores unstructured data as file. The method acts as a primitive operation that is called by
262      * the template method <code>execute(boolean)</code>;
263      * 
264      * @param is The input stream.
265      * @param file Destination file.
266      * @param offset The file offset (-1 on append).
267      * @throws IOException Thrown if IO fails or if at least one resource limit was reached
268      */
269     protected abstract void doStoreFileData(InputStream is, File file, long offset) throws IOException;
270 
271     /***
272      * Getter method for the java bean <code>completed</code>.
273      * 
274      * @return Returns the value of the java bean <code>completed</code>.
275      */
276     public synchronized long getCompleted() {
277         return completed;
278     }
279 
280     /***
281      * Setter method for the java bean <code>completed</code>.
282      * 
283      * @param completed The value of completed to set.
284      */
285     public synchronized void incCompleted(long completed) {
286         this.completed += completed;
287     }
288 
289     /***
290      * Getter method for the java bean <code>fileSize</code>.
291      * 
292      * @return Returns the value of the java bean <code>fileSize</code>.
293      */
294     public long getFileSize() {
295         return fileSize;
296     }
297 
298     /***
299      * Setter method for the java bean <code>fileSize</code>.
300      * 
301      * @param fileSize The value of fileSize to set.
302      */
303     public void setFileSize(long fileSize) {
304         this.fileSize = fileSize;
305     }
306 
307     /***
308      * @return True if abort has been requested.
309      */
310     protected boolean isAbortRequested() {
311         return abortRequested;
312     }
313 
314     /***
315      * Getter Methode fuer die Eigenschaft <code>transferRateLimiter</code>.
316      * 
317      * @return Wert der Eigenschaft <code>transferRateLimiter</code>.
318      */
319     public TransferRateLimiter getTransferRateLimiter() {
320         return transferRateLimiter;
321     }
322 
323     /***
324      * @param transferRateLimiter the transferRateLimiter to set
325      */
326     public void setTransferRateLimiter(TransferRateLimiter transferRateLimiter) {
327         this.transferRateLimiter = transferRateLimiter;
328     }
329 
330 }