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  package org.waarp.openr66.context.filesystem;
21  
22  import io.netty.channel.ChannelFuture;
23  import org.waarp.common.command.exception.CommandAbstractException;
24  import org.waarp.common.digest.FilesystemBasedDigest;
25  import org.waarp.common.exception.FileEndOfTransferException;
26  import org.waarp.common.exception.FileTransferException;
27  import org.waarp.common.file.AbstractDir;
28  import org.waarp.common.file.DataBlock;
29  import org.waarp.common.file.FileUtils;
30  import org.waarp.common.file.filesystembased.FilesystemBasedFileImpl;
31  import org.waarp.common.logging.SysErrLogger;
32  import org.waarp.common.logging.WaarpLogger;
33  import org.waarp.common.logging.WaarpLoggerFactory;
34  import org.waarp.common.utility.WaarpNettyUtil;
35  import org.waarp.openr66.context.ErrorCode;
36  import org.waarp.openr66.context.R66Result;
37  import org.waarp.openr66.context.R66Session;
38  import org.waarp.openr66.context.task.exception.OpenR66RunnerErrorException;
39  import org.waarp.openr66.database.data.DbTaskRunner;
40  import org.waarp.openr66.protocol.configuration.Configuration;
41  import org.waarp.openr66.protocol.exception.OpenR66ProtocolPacketException;
42  import org.waarp.openr66.protocol.exception.OpenR66ProtocolSystemException;
43  import org.waarp.openr66.protocol.localhandler.LocalChannelReference;
44  import org.waarp.openr66.protocol.localhandler.RetrieveRunner;
45  import org.waarp.openr66.protocol.utils.ChannelUtils;
46  
47  import java.io.File;
48  import java.io.FileInputStream;
49  import java.io.FileNotFoundException;
50  import java.io.FileOutputStream;
51  import java.io.IOException;
52  import java.io.RandomAccessFile;
53  import java.security.NoSuchAlgorithmException;
54  import java.util.concurrent.atomic.AtomicBoolean;
55  
56  /**
57   * File representation
58   */
59  public class R66File extends FilesystemBasedFileImpl {
60    /**
61     * Internal Logger
62     */
63    private static final WaarpLogger logger =
64        WaarpLoggerFactory.getLogger(R66File.class);
65  
66    /**
67     * Does the current file is external (i.e. out of R66 base directory)
68     */
69    private boolean isExternal;
70  
71    /**
72     * @param session
73     * @param dir
74     * @param path
75     * @param append
76     *
77     * @throws CommandAbstractException
78     */
79    public R66File(final R66Session session, final R66Dir dir, final String path,
80                   final boolean append) throws CommandAbstractException {
81      super(session, dir, path, append);
82    }
83  
84    /**
85     * This constructor is for External file
86     *
87     * @param session
88     * @param dir
89     * @param path
90     */
91    public R66File(final R66Session session, final R66Dir dir,
92                   final String path) {
93      super(session, dir, path);
94      isExternal = true;
95    }
96  
97    /**
98     * Start the retrieve (send to the remote host the local file)
99     *
100    * @param running When false, should stop the runner
101    *
102    * @throws OpenR66RunnerErrorException
103    * @throws OpenR66ProtocolSystemException
104    */
105   public final synchronized void retrieveBlocking(final AtomicBoolean running)
106       throws OpenR66RunnerErrorException, OpenR66ProtocolSystemException {
107     boolean retrieveDone = false;
108     String errorMesg = "";
109     FilesystemBasedDigest digestGlobal = null;
110     logger.debug("File to retrieve: {}", this);
111     long toRead = 0;
112     try {
113       final long length = length();
114       toRead = (length - getPosition());
115       if (!isReady) {
116         logger.error(
117             "File is not ready to be retrieved: Filename {} isReady {}",
118             getBasename(), isReady);
119         errorMesg = "File is not ready to be retrieved: " + "Filename " +
120                     getBasename() + " isReady " + isReady;
121       } else if (toRead < 0) {
122         logger.error(
123             "File is not ready to be read: Filename {} initialLength {} position {}",
124             getBasename(), length, getPosition());
125         errorMesg =
126             "File is not ready to be read: " + "Filename " + getBasename() +
127             " initialLength " + length + " position " + getPosition();
128       }
129     } catch (final CommandAbstractException e) {
130       logger.warn(e.getMessage());
131     }
132     final LocalChannelReference localChannelReference =
133         getSession().getLocalChannelReference();
134     final FilesystemBasedDigest digestBlock =
135         ((R66Session) session).getDigestBlock();
136     DataBlock block = null;
137     final byte[] buffer =
138         ((R66Session) session).getReusableBuffer(session.getBlockSize());
139     try {
140       if (!isReady) {
141         return;
142       }
143       try {
144         block = readDataBlock(buffer);
145       } catch (final FileEndOfTransferException e) {
146         if (toRead > 0) {
147           // Should not be
148           retrieveDone = false;
149           errorMesg = "File is ready to be retrieved and needs to read but " +
150                       "EndOfTransfer: " + "Filename " + getBasename() +
151                       " isReady " + isReady + " " + "toRead " + toRead +
152                       " position " + getPosition();
153           return;
154         }
155         // Last block (in fact, no data to read)
156         retrieveDone = true;
157         return;
158       }
159       if (block == null) {
160         if (toRead > 0) {
161           // Should not be
162           retrieveDone = false;
163           errorMesg = "File is ready to be retrieved and needs to read but " +
164                       "no Read: " + "Filename " + getBasename() + " isReady " +
165                       isReady + " " + "toRead " + toRead + " position " +
166                       getPosition();
167           return;
168         }
169         // Last block (in fact, no data to read)
170         retrieveDone = true;
171         return;
172       }
173       if (Configuration.configuration.isGlobalDigest()) {
174         try {
175           digestGlobal = new FilesystemBasedDigest(
176               Configuration.configuration.getDigest());
177         } catch (final NoSuchAlgorithmException e2) {
178           // ignore
179         }
180       }
181       ChannelFuture future1 = null;
182       ChannelFuture future2;
183       if (running.get() && !Thread.interrupted()) {
184         if (Configuration.configuration.isGlobalDigest()) {
185           future1 =
186               RetrieveRunner.writeWhenPossible(block, localChannelReference,
187                                                digestGlobal, digestBlock);
188         } else {
189           future1 =
190               RetrieveRunner.writeWhenPossible(block, localChannelReference,
191                                                null, digestBlock);
192         }
193       }
194       // While not last block
195       while (block != null && !block.isEOF() && running.get() &&
196              !Thread.interrupted()) {
197         WaarpNettyUtil.awaitOrInterrupted(future1);
198         if (!future1.isSuccess()) {
199           errorMesg = "Message not written: " + future1.cause() != null?
200               future1.cause().getMessage() : " no cause";
201           return;
202         }
203         if (!running.get() && Thread.interrupted()) {
204           errorMesg = "Running stopped";
205           return;
206         }
207         try {
208           block = readDataBlock(buffer);
209         } catch (final FileEndOfTransferException e) {
210           // Wait for last write
211           WaarpNettyUtil.awaitOrInterrupted(future1);
212           if (future1.isSuccess()) {
213             retrieveDone = true;
214           } else {
215             errorMesg =
216                 "Message not written in loop: " + future1.cause() != null?
217                     future1.cause().getMessage() : " no cause";
218           }
219           return;
220         }
221         if (Configuration.configuration.isGlobalDigest()) {
222           future2 =
223               RetrieveRunner.writeWhenPossible(block, localChannelReference,
224                                                digestGlobal, digestBlock);
225         } else {
226           future2 =
227               RetrieveRunner.writeWhenPossible(block, localChannelReference,
228                                                null, digestBlock);
229         }
230         future1 = future2;
231       }
232       if (!running.get() && Thread.interrupted()) {
233         // stopped
234         errorMesg = "Running stopped step 2";
235         return;
236       }
237       // Wait for last write
238       if (future1 != null) {
239         WaarpNettyUtil.awaitOrInterrupted(future1);
240         if (!future1.isSuccess()) {
241           errorMesg = "Message not written at end: " + future1.cause() != null?
242               future1.cause().getMessage() : " no cause";
243           return;
244         }
245       }
246       retrieveDone = true;
247     } catch (final FileTransferException e) {
248       // An error occurs!
249       getSession().setFinalizeTransfer(false, new R66Result(
250           new OpenR66ProtocolSystemException(e), getSession(), false,
251           ErrorCode.TransferError, getSession().getRunner()));
252     } catch (final OpenR66ProtocolPacketException e) {
253       // An error occurs!
254       getSession().setFinalizeTransfer(false,
255                                        new R66Result(e, getSession(), false,
256                                                      ErrorCode.Internal,
257                                                      getSession().getRunner()));
258     } finally {
259       if (block != null) {
260         block.clear();
261       }
262       try {
263         closeFile();
264       } catch (final CommandAbstractException ignore) {
265         SysErrLogger.FAKE_LOGGER.ignoreLog(ignore);
266       }
267       if (retrieveDone) {
268         String hash = null;
269         if (digestGlobal != null) {
270           hash = FilesystemBasedDigest.getHex(digestGlobal.Final());
271         }
272         try {
273           if (hash == null) {
274             ChannelUtils.writeEndTransfer(localChannelReference);
275           } else {
276             ChannelUtils.writeEndTransfer(localChannelReference, hash);
277           }
278         } catch (final OpenR66ProtocolPacketException e) {
279           // An error occurs!
280           getSession().setFinalizeTransfer(false,
281                                            new R66Result(e, getSession(), false,
282                                                          ErrorCode.Internal,
283                                                          getSession().getRunner()));
284         }
285       } else {
286         // An error occurs!
287         logger.error("Cannot send file: " + errorMesg);
288         getSession().setFinalizeTransfer(false, new R66Result(
289             new OpenR66ProtocolSystemException(
290                 "Transfer in error: " + errorMesg), getSession(), false,
291             ErrorCode.TransferError, getSession().getRunner()));
292       }
293     }
294   }
295 
296   /**
297    * This method is a good to have in a true FileInterface implementation.
298    *
299    * @return the File associated with the current FileInterface operation
300    */
301   public final File getTrueFile() {
302     if (isExternal) {
303       if (currentRealFile == null) {
304         currentRealFile = new File(currentFile);
305       }
306       return currentRealFile;
307     }
308     try {
309       if (currentRealFile == null) {
310         currentRealFile = getFileFromPath(getFile());
311       }
312       return currentRealFile;
313     } catch (final CommandAbstractException e) {
314       logger.warn("Exception while getting file: " + this + " : {}",
315                   e.getMessage());
316       return null;
317     }
318   }
319 
320   /**
321    * @return the basename of the current file
322    */
323   public final String getBasename() {
324     getTrueFile();
325     return currentRealFile.getName();
326   }
327 
328   /**
329    * @param path
330    *
331    * @return the basename from the given path
332    */
333   public static String getBasename(final String path) {
334     int pos = path.lastIndexOf('/');
335     final int pos2 = path.lastIndexOf('\\');
336     if (pos2 > pos) {
337       pos = pos2;
338     }
339     if (pos > 0) {
340       return path.substring(pos + 1);
341     }
342     return path;
343   }
344 
345   @Override
346   public final R66Session getSession() {
347     return (R66Session) session;
348   }
349 
350   @Override
351   public final boolean canRead() throws CommandAbstractException {
352     if (isExternal) {
353       if (currentRealFile == null) {
354         currentRealFile = new File(currentFile);
355       }
356       logger.debug("Final File: {} CanRead: {}", currentRealFile,
357                    currentRealFile.canRead());
358       return canRead(currentRealFile);
359     }
360     return super.canRead();
361   }
362 
363   @Override
364   public final boolean canWrite() throws CommandAbstractException {
365     if (isExternal) {
366       if (currentRealFile == null) {
367         currentRealFile = new File(currentFile);
368       }
369       if (currentRealFile.exists()) {
370         return currentRealFile.canWrite();
371       }
372       return currentRealFile.getParentFile().canWrite();
373     }
374     return super.canWrite();
375   }
376 
377   @Override
378   public final boolean delete() throws CommandAbstractException {
379     if (isExternal) {
380       if (currentRealFile == null) {
381         currentRealFile = new File(currentFile);
382       }
383       checkIdentify();
384       if (!isReady) {
385         return false;
386       }
387       if (!currentRealFile.exists()) {
388         return true;
389       }
390       closeFile();
391       return currentRealFile.delete();
392     }
393     return super.delete();
394   }
395 
396   @Override
397   public final boolean exists() throws CommandAbstractException {
398     if (isExternal) {
399       if (currentRealFile == null) {
400         currentRealFile = new File(currentFile);
401       }
402       return exists(currentRealFile);
403     }
404     return super.exists();
405   }
406 
407   @Override
408   protected final FileInputStream getFileInputStream() {
409     if (!isExternal) {
410       return super.getFileInputStream();
411     }
412     if (!isReady) {
413       return null;
414     }
415     final File trueFile = getTrueFile();
416     @SuppressWarnings("resource")
417     FileInputStream fileInputStream = null;
418     try {
419       fileInputStream = new FileInputStream(trueFile);//NOSONAR
420       final long pos = getPosition();
421       if (pos > 0) {
422         final long read = fileInputStream.skip(pos);
423         if (read != pos) {
424           logger.warn("Cannot ensure position: {} while is {}", pos, read);
425         }
426       }
427     } catch (final FileNotFoundException e) {
428       logger.error("FileInterface not found in getFileInputStream: {}",
429                    e.getMessage());
430       return null;
431     } catch (final IOException e) {
432       logger.error("Change position in getFileInputStream: {}", e.getMessage());
433       return null;
434     }
435     return fileInputStream;
436   }
437 
438   @Override
439   protected final RandomAccessFile getRandomFile() {
440     if (!isExternal) {
441       return super.getRandomFile();
442     }
443     if (!isReady) {
444       return null;
445     }
446     final File trueFile = getTrueFile();
447     final RandomAccessFile raf;
448     try {
449       raf = new RandomAccessFile(trueFile, "rw");//NOSONAR
450       raf.seek(getPosition());
451     } catch (final FileNotFoundException e) {
452       logger.error("File not found in getRandomFile: {}", e.getMessage());
453       return null;
454     } catch (final IOException e) {
455       logger.error("Change position in getRandomFile: {}", e.getMessage());
456       return null;
457     }
458     return raf;
459   }
460 
461   /**
462    * Returns the FileOutputStream in Out mode associated with the current
463    * file.
464    *
465    * @param append True if the FileOutputStream should be in append
466    *     mode
467    *
468    * @return the FileOutputStream (OUT)
469    */
470   @Override
471   protected final FileOutputStream getFileOutputStream(final boolean append) {
472     if (!isExternal) {
473       return super.getFileOutputStream(append);
474     }
475     if (!isReady) {
476       return null;
477     }
478     final File trueFile = getTrueFile();
479     if (trueFile == null) {
480       logger.error("File is unknown so cannot get OutputStream");
481       return null;
482     }
483     if (getPosition() > 0) {
484       if (trueFile.length() < getPosition()) {
485         logger.error(
486             "Cannot Change position in getFileOutputStream: file is smaller than required position");
487         return null;
488       }
489       final RandomAccessFile raf = getRandomFile();
490       if (raf == null) {
491         logger.error("File is unknown so cannot get OutputStream");
492         return null;
493       }
494       try {
495         raf.setLength(getPosition());
496         org.waarp.common.file.FileUtils.close(raf);
497       } catch (final IOException e) {
498         logger.error("Change position in getFileOutputStream: {}",
499                      e.getMessage());
500         return null;
501       }
502     }
503     final FileOutputStream fos;
504     try {
505       fos = new FileOutputStream(trueFile, append);
506     } catch (final FileNotFoundException e) {
507       logger.error("File not found in getRandomFile: {}", e.getMessage());
508       return null;
509     }
510     return fos;
511   }
512 
513   @Override
514   public final boolean isDirectory() throws CommandAbstractException {
515     if (isExternal) {
516       if (currentRealFile == null) {
517         currentRealFile = new File(currentFile);
518       }
519       return isDirectory(currentRealFile);
520     }
521     return super.isDirectory();
522   }
523 
524   @Override
525   public final boolean isFile() throws CommandAbstractException {
526     if (isExternal) {
527       if (currentRealFile == null) {
528         currentRealFile = new File(currentFile);
529       }
530       return isFile(currentRealFile);
531     }
532     return super.isFile();
533   }
534 
535   @Override
536   public final long length() throws CommandAbstractException {
537     if (isExternal) {
538       if (currentRealFile == null) {
539         currentRealFile = new File(currentFile);
540       }
541       if (canRead(currentRealFile)) {
542         return currentRealFile.length();
543       } else {
544         return -1;
545       }
546     }
547     return super.length();
548   }
549 
550   protected final String getFullInDir() {
551     final DbTaskRunner runner = getSession().getRunner();
552     if (runner != null) {
553       final R66Dir dir = new R66Dir(getSession());
554       try {
555         dir.changeDirectory(runner.getRule().getRecvPath());
556         return dir.getFullPath();
557       } catch (final CommandAbstractException ignored) {
558         // nothing
559       }
560     }
561     return null;
562   }
563 
564   @Override
565   public final boolean renameTo(final String path)
566       throws CommandAbstractException {
567     if (!isExternal) {
568       return super.renameTo(path);
569     }
570     checkIdentify();
571     if (!isReady) {
572       logger.warn("File not ready: {}", this);
573       return false;
574     }
575     final File file = getTrueFile();
576     if (file != null && canRead(file)) {
577       File newFile = getFileFromPath(path);
578       File parentFile = newFile.getParentFile();
579       if (parentFile == null) {
580         final String dir = getFullInDir();
581         if (dir != null) {
582           newFile = new File(dir, newFile.getPath());
583           parentFile = newFile.getParentFile();
584         }
585       }
586       if (newFile.exists()) {
587         logger.warn("Target file already exists: " + newFile.getAbsolutePath());
588         return false;
589       }
590       if (newFile.getAbsolutePath().equals(file.getAbsolutePath())) {
591         // already in the right position
592         isReady = true;
593         return true;
594       }
595       if (parentFile != null && parentFile.canWrite()) {
596         if (renameTo(file, newFile)) {
597           return false;
598         }
599         currentFile = getRelativePath(newFile);
600         currentRealFile = newFile;
601         isExternal = false;
602         isReady = true;
603         logger.debug("File renamed to: {} and real position: {}", this,
604                      newFile);
605         return true;
606       }
607     }
608     logger.warn("Cannot read file: {}", file);
609     return false;
610   }
611 
612   private boolean renameTo(final File file, final File newFile)
613       throws CommandAbstractException {
614     if (!file.renameTo(newFile)) {
615       FileUtils.copy(file, newFile, true, false);
616       return true;
617     }
618     return false;
619   }
620 
621   /**
622    * Move the current file to the path as destination
623    *
624    * @param path
625    * @param external if True, the path is outside authentication
626    *     control
627    *
628    * @return True if the operation is done
629    *
630    * @throws CommandAbstractException
631    */
632   public final boolean renameTo(final String path, final boolean external)
633       throws CommandAbstractException {
634     if (!external) {
635       return renameTo(path);
636     }
637     checkIdentify();
638     if (!isReady) {
639       return false;
640     }
641     final File file = getTrueFile();
642     if (file != null && canRead(file)) {
643       File newFile = new File(path);
644       File parentFile = newFile.getParentFile();
645       if (parentFile == null) {
646         final String dir = getFullInDir();
647         if (dir != null) {
648           newFile = new File(dir, newFile.getPath());
649           parentFile = newFile.getParentFile();
650         }
651       }
652       if (newFile.exists()) {
653         logger.warn("Target file already exists: " + newFile.getAbsolutePath());
654         return false;
655       }
656       if (newFile.getAbsolutePath().equals(file.getAbsolutePath())) {
657         // already in the right position
658         isReady = true;
659         return true;
660       }
661       if (parentFile != null && parentFile.canWrite()) {
662         if (renameTo(file, newFile)) {
663           return false;
664         }
665         currentFile = AbstractDir.normalizePath(newFile.getAbsolutePath());
666         currentRealFile = newFile;
667         isExternal = true;
668         isReady = true;
669         return true;
670       }
671       logger.error("Cannot write to parent directory: {}", newFile.getParent());
672     }
673     logger.error("Cannot read file: {}", file);
674     return false;
675   }
676 
677   /**
678    * Replace the current file with the new filename after closing the previous
679    * one.
680    *
681    * @param filename
682    * @param isExternal
683    *
684    * @throws CommandAbstractException
685    */
686   public final void replaceFilename(final String filename,
687                                     final boolean isExternal)
688       throws CommandAbstractException {
689     closeFile();
690     currentFile = filename;
691     currentRealFile = null;
692     this.isExternal = isExternal;
693     isReady = true;
694   }
695 
696   @Override
697   public final synchronized boolean closeFile()
698       throws CommandAbstractException {
699     final boolean status = super.closeFile();
700     // FORCE re-open file
701     isReady = true;
702     return status;
703   }
704 
705   /**
706    * @return True if this file is outside OpenR66 Base directory
707    */
708   public final boolean isExternal() {
709     return isExternal;
710   }
711 
712   @Override
713   public String toString() {
714     return "File: " + currentFile + " Ready " + isReady + " isExternal " +
715            isExternal + ' ' + getPosition();
716   }
717 }