View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  
21  package org.waarp.http.protocol;
22  
23  import org.waarp.common.command.exception.CommandAbstractException;
24  import org.waarp.common.digest.FilesystemBasedDigest;
25  import org.waarp.common.digest.FilesystemBasedDigest.DigestAlgo;
26  import org.waarp.common.logging.WaarpLogger;
27  import org.waarp.common.logging.WaarpLoggerFactory;
28  import org.waarp.common.lru.ConcurrentUtility;
29  import org.waarp.common.utility.ParametersChecker;
30  import org.waarp.common.utility.WaarpStringUtils;
31  import org.waarp.common.utility.WaarpSystemUtil;
32  import org.waarp.http.protocol.servlet.HttpAuthent;
33  import org.waarp.openr66.context.R66BusinessInterface;
34  import org.waarp.openr66.context.filesystem.R66Dir;
35  import org.waarp.openr66.context.filesystem.R66File;
36  import org.waarp.openr66.context.task.exception.OpenR66RunnerErrorException;
37  import org.waarp.openr66.database.data.DbTaskRunner;
38  
39  import java.io.File;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.RandomAccessFile;
43  import java.util.Set;
44  import java.util.UUID;
45  
46  import static org.waarp.openr66.context.R66FiniteDualStates.*;
47  
48  /**
49   * Http Resumable session
50   */
51  public class HttpResumableSession extends HttpSessionAbstract {
52    private static final WaarpLogger logger =
53        WaarpLoggerFactory.getLogger(HttpResumableSession.class);
54  
55    private final Set<HttpResumableChunkNumber> uploadedChunks =
56        ConcurrentUtility.newConcurrentSet();
57    private final HttpResumableInfo httpResumableInfo;
58    private long size = 0L;
59  
60    /**
61     * Constructor for an Http Resumable Session
62     *
63     * @param resumableInfo
64     * @param rulename
65     * @param comment
66     * @param authent already initialized
67     *
68     * @throws IllegalArgumentException if something wrong happens
69     */
70    HttpResumableSession(final HttpResumableInfo resumableInfo,
71                         final String rulename, final String comment,
72                         final HttpAuthent authent)
73        throws IllegalArgumentException {
74      super(authent);
75      this.httpResumableInfo = resumableInfo;
76      if (!WaarpSystemUtil.isJunit()) {
77        final R66BusinessInterface business =
78            checkAuthentR66Business(this, session, authent);
79        final DbTaskRunner runner =
80            getDbTaskRunner(authent.getUserId(), rulename, comment, business);
81        preTasks(business, runner);
82      }
83    }
84  
85    /**
86     * Adapted method to resumable context
87     *
88     * @param user
89     * @param rulename
90     * @param comment
91     * @param business
92     *
93     * @return the DbTaskRunner
94     */
95    private DbTaskRunner getDbTaskRunner(final String user, final String rulename,
96                                         final String comment,
97                                         final R66BusinessInterface business) {
98      final long uuid = UUID.nameUUIDFromBytes(
99                                httpResumableInfo.getIdentifier().getBytes(WaarpStringUtils.UTF8))
100                           .getMostSignificantBits();
101     return getDbTaskRunner(user, httpResumableInfo.getFilename(), rulename,
102                            uuid, comment, httpResumableInfo.getChunkSize(),
103                            business, true);
104   }
105 
106   /**
107    * @return the current HttpResumableInfo
108    */
109   public HttpResumableInfo getHttpResumableInfo() {
110     return httpResumableInfo;
111   }
112 
113   /**
114    * Try to write the data according to resumbaleInfo
115    *
116    * @param resumableInfo
117    * @param stream
118    *
119    * @return true if Write is OK
120    *
121    * @throws IOException
122    */
123   public final boolean tryWrite(final HttpResumableInfo resumableInfo,
124                                 final InputStream stream) throws IOException {
125     if (!session.isAuthenticated() || !session.isReady()) {
126       logger.error("Not authenticated or not Ready");
127       return false;
128     }
129     if (!valid(resumableInfo)) {
130       return false;
131     }
132     final HttpResumableChunkNumber chunkNumber =
133         new HttpResumableChunkNumber(resumableInfo.getChunkNumber());
134     if (uploadedChunks.contains(chunkNumber)) {
135       return false;
136     }
137     write(resumableInfo, stream);
138     uploadedChunks.add(chunkNumber);
139     session.getRunner().incrementRank();
140     return true;
141   }
142 
143   /**
144    * Check if the resumableInfo is valid compared to current session
145    *
146    * @param resumableInfo
147    *
148    * @return true if ok
149    */
150   public final boolean valid(final HttpResumableInfo resumableInfo) {
151     return (resumableInfo.getChunkSize() > 0 &&
152             resumableInfo.getTotalSize() > 0 &&
153             resumableInfo.getChunkNumber() > 0 &&
154             ParametersChecker.isNotEmpty(resumableInfo.getIdentifier()) &&
155             ParametersChecker.isNotEmpty(resumableInfo.getFilename()) &&
156             ParametersChecker.isNotEmpty(resumableInfo.getRelativePath()) &&
157             httpResumableInfo.isCompatible(resumableInfo));
158   }
159 
160   /**
161    * Real write to the final file
162    *
163    * @param info
164    * @param stream
165    *
166    * @throws IOException
167    */
168   private void write(final HttpResumableInfo info, final InputStream stream)
169       throws IOException {
170     final File file = session.getFile().getTrueFile();
171     RandomAccessFile raf = null;
172     try {
173       raf = new RandomAccessFile(file, "rw");
174       //Seek to position
175       raf.seek((info.getChunkNumber() - 1) * (long) info.getChunkSize());
176       final byte[] bytes = new byte[info.getChunkSize()];
177       while (true) {
178         final int r = stream.read(bytes);
179         if (r < 0) {
180           break;
181         }
182         raf.write(bytes, 0, r);
183         size += r;
184       }
185     } finally {
186       if (raf != null) {
187         raf.close();
188       }
189       stream.close();
190     }
191 
192   }
193 
194   /**
195    * Check if the resumableInfo block is already written (previously received)
196    *
197    * @param resumableInfo
198    *
199    * @return True if contained
200    */
201   public final boolean contains(final HttpResumableInfo resumableInfo) {
202     final HttpResumableChunkNumber chunkNumber =
203         new HttpResumableChunkNumber(resumableInfo.getChunkNumber());
204     return uploadedChunks.contains(chunkNumber);
205   }
206 
207   /**
208    * Check if the current upload is finished or not
209    *
210    * @param sha256 if not empty, contains the sha256 in hex format
211    *
212    * @return True if finished
213    */
214   public final boolean checkIfUploadFinished(final String sha256) {
215     logger.debug("Write until now: {} for {}", size,
216                  httpResumableInfo.getTotalSize());
217     //check if upload finished
218     if (size != httpResumableInfo.getTotalSize()) {
219       return false;
220     }
221     if (session.getState() == CLOSEDCHANNEL) {
222       return true;
223     }
224     logger.debug("Final block received! {}", session);
225     session.newState(ENDTRANSFERS);
226     //Upload finished, change filename.
227     final R66File r66File = session.getFile();
228     final File file = r66File.getTrueFile();
229     if (file.isFile()) {
230       // Now if sha256 is given, compute it and compare
231       if (ParametersChecker.isNotEmpty(sha256)) {
232         try {
233           final byte[] bin =
234               FilesystemBasedDigest.getHash(file, false, DigestAlgo.SHA256);
235           if (!FilesystemBasedDigest.digestEquals(sha256, bin)) {
236             logger.error("Digests differs: {} {}", sha256,
237                          FilesystemBasedDigest.getHex(bin));
238             error(new OpenR66RunnerErrorException("Digest differs"),
239                   session.getBusinessObject());
240           }
241           logger.info("Digest OK");
242         } catch (final IOException ignore) {
243           logger.warn(ignore);
244         }
245       } else {
246         logger.info("NO DIGEST given");
247       }
248       final DbTaskRunner runner = session.getRunner();
249       try {
250         final String finalpath = R66Dir.getFinalUniqueFilename(r66File);
251         logger.debug("File to move from {} to {}",
252                      r66File.getTrueFile().getAbsolutePath(), finalpath);
253         if (!r66File.renameTo(runner.getRule().setRecvPath(finalpath))) {
254           logger.error("Cannot move file to final position {}",
255                        runner.getRule().setRecvPath(finalpath));
256         }
257         runner.setFilename(r66File.getFile());
258         runner.saveStatus();
259         runPostTask();
260         authent.finalizeTransfer(this, session);
261       } catch (final OpenR66RunnerErrorException e) {
262         error(e, session.getBusinessObject());
263       } catch (final CommandAbstractException e) {
264         error(e, session.getBusinessObject());
265       }
266       return true;
267     }
268     return true;
269   }
270 
271   @Override
272   public String toString() {
273     return "RS: {" + uploadedChunks.toString() + ", " + session.toString() +
274            ", " + httpResumableInfo + "}";
275   }
276 }