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.common.file.filesystembased;
21  
22  import org.waarp.common.command.exception.CommandAbstractException;
23  import org.waarp.common.exception.FileEndOfTransferException;
24  import org.waarp.common.exception.FileTransferException;
25  import org.waarp.common.file.AbstractDir;
26  import org.waarp.common.file.AbstractFile;
27  import org.waarp.common.file.DataBlock;
28  import org.waarp.common.file.DirInterface;
29  import org.waarp.common.file.FileUtils;
30  import org.waarp.common.file.SessionInterface;
31  import org.waarp.common.logging.SysErrLogger;
32  import org.waarp.common.logging.WaarpLogger;
33  import org.waarp.common.logging.WaarpLoggerFactory;
34  
35  import java.io.File;
36  import java.io.FileInputStream;
37  import java.io.FileNotFoundException;
38  import java.io.FileOutputStream;
39  import java.io.IOException;
40  import java.io.RandomAccessFile;
41  
42  /**
43   * File implementation for Filesystem Based
44   */
45  public abstract class FilesystemBasedFileImpl extends AbstractFile {
46    private static final String ERROR_DURING_GET = "Error during get:";
47  
48    private static final String INTERNAL_ERROR_FILE_IS_NOT_READY =
49        "Internal error, file is not ready";
50  
51    private static final String NO_FILE_IS_READY = "No file is ready";
52  
53    /**
54     * Internal Logger
55     */
56    private static final WaarpLogger logger =
57        WaarpLoggerFactory.getLogger(FilesystemBasedFileImpl.class);
58  
59    /**
60     * SessionInterface
61     */
62    protected final SessionInterface session;
63  
64    /**
65     * DirInterface associated with this file at creation. It is not necessary
66     * the
67     * directory that owns this file.
68     */
69    private final FilesystemBasedDirImpl dir;
70  
71    /**
72     * {@link FilesystemBasedAuthImpl}
73     */
74    private final FilesystemBasedAuthImpl auth;
75  
76    /**
77     * Current file if any
78     */
79    protected String currentFile;
80    /**
81     * Current Real File if any
82     */
83    protected File currentRealFile = null;
84  
85    /**
86     * Is this file in append mode
87     */
88    protected boolean isAppend;
89  
90    /**
91     * Valid Position of this file
92     */
93    private long position;
94  
95    /**
96     * FileOutputStream Out
97     */
98    private FileOutputStream fileOutputStream;
99    /**
100    * FileInputStream In
101    */
102   private FileInputStream fileInputStream;
103 
104   private byte[] reusableBytes;
105 
106   /**
107    * @param session
108    * @param dir It is not necessary the directory that owns this file.
109    * @param path
110    * @param append
111    *
112    * @throws CommandAbstractException
113    */
114   protected FilesystemBasedFileImpl(final SessionInterface session,
115                                     final FilesystemBasedDirImpl dir,
116                                     final String path, final boolean append)
117       throws CommandAbstractException {
118     this.session = session;
119     auth = (FilesystemBasedAuthImpl) session.getAuth();
120     this.dir = dir;
121     currentFile = path;
122     isAppend = append;
123     currentRealFile = getFileFromPath(path);
124     if (append) {
125       try {
126         setPosition(currentRealFile.length());
127       } catch (final IOException e) {
128         // not ready
129         return;
130       }
131     } else {
132       try {
133         setPosition(0);
134       } catch (final IOException ignored) {
135         // nothing
136       }
137     }
138     isReady = true;
139   }
140 
141   /**
142    * Special constructor for possibly external file
143    *
144    * @param session
145    * @param dir It is not necessary the directory that owns this file.
146    * @param path
147    */
148   protected FilesystemBasedFileImpl(final SessionInterface session,
149                                     final FilesystemBasedDirImpl dir,
150                                     final String path) {
151     this.session = session;
152     auth = (FilesystemBasedAuthImpl) session.getAuth();
153     this.dir = dir;
154     currentFile = path;
155     currentRealFile = null;
156     isReady = true;
157     isAppend = false;
158     position = 0;
159   }
160 
161   @Override
162   public final void clear() throws CommandAbstractException {
163     super.clear();
164     currentFile = null;
165     currentRealFile = null;
166     isAppend = false;
167   }
168 
169   @Override
170   public SessionInterface getSession() {
171     return session;
172   }
173 
174   @Override
175   public final DirInterface getDir() {
176     return dir;
177   }
178 
179   /**
180    * Get the File from this path, checking first its validity
181    *
182    * @param path
183    *
184    * @return the FileInterface
185    *
186    * @throws CommandAbstractException
187    */
188   protected final File getFileFromPath(final String path)
189       throws CommandAbstractException {
190     final String newdir = getDir().validatePath(path);
191     if (dir.isAbsolute(newdir)) {
192       return new File(newdir);
193     }
194     final String truedir = auth.getAbsolutePath(newdir);
195     final File file = new File(truedir);
196     logger.debug("Final File: {} CanRead: {}", truedir, file.canRead());
197     return file;
198   }
199 
200   /**
201    * Get the relative path (without mount point)
202    *
203    * @param file
204    *
205    * @return the relative path
206    */
207   protected final String getRelativePath(final File file) {
208     return auth.getRelativePath(
209         AbstractDir.normalizePath(file.getAbsolutePath()));
210   }
211 
212   /**
213    * Adapt File.isDirectory() to leverage synchronization error with filesystem
214    *
215    * @param file
216    *
217    * @return as with File.isDirectory()
218    */
219   public static boolean isDirectory(final File file) {
220     for (int i = 0; i < 3; i++) {
221       if (file.isDirectory()) {
222         return true;
223       }
224       try {
225         Thread.sleep(10);
226       } catch (final InterruptedException ignored) { //NOSONAR
227         SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
228       }
229     }
230     return false;
231   }
232 
233   @Override
234   public boolean isDirectory() throws CommandAbstractException {
235     checkIdentify();
236     if (currentRealFile == null) {
237       currentRealFile = getFileFromPath(currentFile);
238     }
239     return isDirectory(currentRealFile);
240   }
241 
242   /**
243    * Adapt File.isFile() to leverage synchronization error with filesystem
244    *
245    * @param file
246    *
247    * @return as with File.isFile()
248    */
249   public static boolean isFile(final File file) {
250     for (int i = 0; i < 3; i++) {
251       if (file.isFile()) {
252         return true;
253       }
254       try {
255         Thread.sleep(10);
256       } catch (final InterruptedException ignored) { //NOSONAR
257         SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
258       }
259     }
260     return false;
261   }
262 
263   @Override
264   public boolean isFile() throws CommandAbstractException {
265     checkIdentify();
266     if (currentRealFile == null) {
267       currentRealFile = getFileFromPath(currentFile);
268     }
269     return isFile(currentRealFile);
270   }
271 
272   @Override
273   public final String getFile() throws CommandAbstractException {
274     checkIdentify();
275     return currentFile;
276   }
277 
278   @Override
279   public synchronized boolean closeFile() throws CommandAbstractException {
280     if (fileInputStream != null) {
281       FileUtils.close(fileInputStream);
282       fileInputStream = null;
283     }
284     if (reusableBytes != null) {
285       reusableBytes = null;
286     }
287     if (fileOutputStream != null) {
288       FileUtils.close(fileOutputStream);
289       fileOutputStream = null;
290     }
291     position = 0;
292     isReady = false;
293     // Do not clear the filename itself
294     return true;
295   }
296 
297   @Override
298   public final synchronized boolean abortFile()
299       throws CommandAbstractException {
300     if (isInWriting() &&
301         ((FilesystemBasedFileParameterImpl) getSession().getFileParameter()).deleteOnAbort) {
302       delete();
303     }
304     closeFile();
305     return true;
306   }
307 
308   @Override
309   public long length() throws CommandAbstractException {
310     checkIdentify();
311     if (!isReady) {
312       return -1;
313     }
314     if (!exists()) {
315       return -1;
316     }
317     if (currentRealFile == null) {
318       currentRealFile = getFileFromPath(currentFile);
319     }
320     return currentRealFile.length();
321   }
322 
323   @Override
324   public final synchronized boolean isInReading() {
325     if (!isReady) {
326       return false;
327     }
328     return fileInputStream != null;
329   }
330 
331   @Override
332   public final synchronized boolean isInWriting() {
333     if (!isReady) {
334       return false;
335     }
336     return fileOutputStream != null;
337   }
338 
339   /**
340    * Adapt File.canRead() to leverage synchronization error with filesystem
341    *
342    * @param file
343    *
344    * @return as with File.canRead()
345    */
346   public static boolean canRead(final File file) {
347     for (int i = 0; i < 3; i++) {
348       if (file.canRead()) {
349         return true;
350       }
351       try {
352         Thread.sleep(10);
353       } catch (final InterruptedException ignored) { //NOSONAR
354         SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
355       }
356     }
357     return false;
358   }
359 
360   @Override
361   public boolean canRead() throws CommandAbstractException {
362     checkIdentify();
363     if (!isReady) {
364       return false;
365     }
366     if (currentRealFile == null) {
367       currentRealFile = getFileFromPath(currentFile);
368     }
369     return canRead(currentRealFile);
370   }
371 
372   @Override
373   public boolean canWrite() throws CommandAbstractException {
374     checkIdentify();
375     if (!isReady) {
376       return false;
377     }
378     if (currentRealFile == null) {
379       currentRealFile = getFileFromPath(currentFile);
380     }
381     if (currentRealFile.exists()) {
382       return currentRealFile.canWrite();
383     }
384     return currentRealFile.getParentFile().canWrite();
385   }
386 
387   /**
388    * Adapt File.exists() to leverage synchronization error with filesystem
389    *
390    * @param file
391    *
392    * @return as with File.exists()
393    */
394   public static boolean exists(final File file) {
395     for (int i = 0; i < 3; i++) {
396       if (file.exists()) {
397         return true;
398       }
399       try {
400         Thread.sleep(10);
401       } catch (final InterruptedException ignored) { //NOSONAR
402         SysErrLogger.FAKE_LOGGER.ignoreLog(ignored);
403       }
404     }
405     return false;
406   }
407 
408   @Override
409   public boolean exists() throws CommandAbstractException {
410     checkIdentify();
411     if (!isReady) {
412       return false;
413     }
414     if (currentRealFile == null) {
415       currentRealFile = getFileFromPath(currentFile);
416     }
417     return exists(currentRealFile);
418   }
419 
420   @Override
421   public boolean delete() throws CommandAbstractException {
422     checkIdentify();
423     if (!isReady) {
424       return false;
425     }
426     if (!exists()) {
427       return true;
428     }
429     closeFile();
430     if (currentRealFile == null) {
431       currentRealFile = getFileFromPath(currentFile);
432     }
433     return currentRealFile.delete();
434   }
435 
436   @Override
437   public boolean renameTo(final String path) throws CommandAbstractException {
438     checkIdentify();
439     if (!isReady) {
440       logger.warn("File not ready: {}", this);
441       return false;
442     }
443     if (currentRealFile == null) {
444       currentRealFile = getFileFromPath(currentFile);
445     }
446     if (canRead(currentRealFile)) {
447       final File newFile = getFileFromPath(path);
448       if (newFile.exists()) {
449         logger.warn("Target file already exists: " + newFile.getAbsolutePath());
450         return false;
451       }
452       if (newFile.getAbsolutePath().equals(currentRealFile.getAbsolutePath())) {
453         // already in the right position
454         isReady = true;
455         return true;
456       }
457       if (newFile.getParentFile().canWrite()) {
458         if (!currentRealFile.renameTo(newFile)) {
459           FileUtils.copy(currentRealFile, newFile, true, false);
460         }
461         currentFile = getRelativePath(newFile);
462         currentRealFile = newFile;
463         isReady = true;
464         logger.debug("File renamed to: {} and real position: {}", this,
465                      newFile);
466         return true;
467       } else {
468         logger.warn("Cannot write file: {} from {}", newFile, currentFile);
469         return false;
470       }
471     }
472     logger.warn("Cannot read file: {}", currentFile);
473     return false;
474   }
475 
476   @Override
477   public final synchronized DataBlock readDataBlock()
478       throws FileTransferException, FileEndOfTransferException {
479     if (isReady) {
480       return getByteBlock(getSession().getBlockSize());
481     }
482     throw new FileTransferException(NO_FILE_IS_READY);
483   }
484 
485   @Override
486   public final synchronized DataBlock readDataBlock(final byte[] bufferGiven)
487       throws FileTransferException, FileEndOfTransferException {
488     if (isReady) {
489       return getByteBlock(bufferGiven);
490     }
491     throw new FileTransferException(NO_FILE_IS_READY);
492   }
493 
494   @Override
495   public final synchronized void writeDataBlock(final DataBlock dataBlock)
496       throws FileTransferException {
497     if (isReady) {
498       if (dataBlock.isEOF()) {
499         writeBlockEnd(dataBlock.getByteBlock(), dataBlock.getOffset(),
500                       dataBlock.getByteCount());
501         return;
502       }
503       writeBlock(dataBlock.getByteBlock(), dataBlock.getOffset(),
504                  dataBlock.getByteCount());
505       return;
506     }
507     throw new FileTransferException(
508         "No file is ready while trying to write: " + dataBlock);
509   }
510 
511   /**
512    * Return the current position in the FileInterface. In write mode, it is
513    * the
514    * current file length.
515    *
516    * @return the position
517    */
518   public final synchronized long getPosition() {
519     return position;
520   }
521 
522   /**
523    * Change the position in the file.
524    *
525    * @param position the position to set
526    *
527    * @throws IOException
528    */
529   @Override
530   public final synchronized void setPosition(final long position)
531       throws IOException {
532     if (this.position != position) {
533       this.position = position;
534       if (fileInputStream != null) {
535         FileUtils.close(fileInputStream);
536         fileInputStream = getFileInputStream();
537       }
538       if (fileOutputStream != null) {
539         FileUtils.close(fileOutputStream);
540         fileOutputStream = getFileOutputStream(true);
541         if (fileOutputStream == null) {
542           throw new IOException("File cannot changed of Position");
543         }
544       }
545     }
546   }
547 
548   /**
549    * Write the current FileInterface with the given byte array. The file is not
550    * limited to 2^32 bytes since this
551    * write operation is in add mode.
552    * <p>
553    * In case of error, the current already written blocks are maintained and
554    * the
555    * position is not changed.
556    *
557    * @param buffer added to the file
558    *
559    * @throws FileTransferException
560    */
561   private synchronized void writeBlock(final byte[] buffer, final int offset,
562                                        final int length)
563       throws FileTransferException {
564     if (length > 0 && !isReady) {
565       throw new FileTransferException(NO_FILE_IS_READY);
566     }
567     // An empty buffer is allowed
568     if (buffer == null || length == 0) {
569       return;// could do FileEndOfTransfer ?
570     }
571     if (fileOutputStream == null) {
572       fileOutputStream = getFileOutputStream(position > 0);
573     }
574     if (fileOutputStream == null) {
575       throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
576     }
577     try {
578       fileOutputStream.write(buffer, offset, length);
579     } catch (final IOException e2) {
580       logger.error("Error during write: {}", e2.getMessage());
581       try {
582         closeFile();
583       } catch (final CommandAbstractException ignored) {
584         // nothing
585       }
586       // NO this.realFile.delete(); NO DELETE SINCE BY BLOCK IT CAN BE
587       // REDO
588       throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
589     }
590     position += length;
591   }
592 
593   /**
594    * End the Write of the current FileInterface with the given byte array. The
595    * file
596    * is not limited to 2^32 bytes
597    * since this write operation is in add mode.
598    *
599    * @param buffer added to the file
600    *
601    * @throws FileTransferException
602    */
603   private synchronized void writeBlockEnd(final byte[] buffer, final int offset,
604                                           final int length)
605       throws FileTransferException {
606     writeBlock(buffer, offset, length);
607     try {
608       closeFile();
609     } catch (final CommandAbstractException e) {
610       throw new FileTransferException("Close in error", e);
611     }
612   }
613 
614   private void checkByteBufSize(final int size) {
615     if (reusableBytes == null || reusableBytes.length != size) {
616       reusableBytes = new byte[size];
617     }
618   }
619 
620   /**
621    * Get the current block of bytes of the current FileInterface. There is
622    * therefore no limitation of the file
623    * size to 2^32 bytes.
624    * <p>
625    * The returned block is limited to sizeblock. If the returned block is less
626    * than sizeblock length, through lastReadSize, it is the
627    * last block to read.
628    *
629    * @param sizeblock is the limit size for the block array
630    *
631    * @return the resulting DataBlock (even empty or partial)
632    *
633    * @throws FileTransferException
634    * @throws FileEndOfTransferException
635    */
636   private synchronized DataBlock getByteBlock(final int sizeblock)
637       throws FileTransferException, FileEndOfTransferException {
638     if (!isReady) {
639       throw new FileTransferException(NO_FILE_IS_READY);
640     }
641     if (fileInputStream == null) {
642       checkByteBufSize(sizeblock);
643     }
644     return getByteBlock(reusableBytes);
645   }
646 
647   /**
648    * Get the current block of bytes of the current FileInterface. There is
649    * therefore no limitation of the file
650    * size to 2^32 bytes.
651    * <p>
652    * The returned block is limited to sizeblock. If the returned block is less
653    * than sizeblock length, through lastReadSize, it is the
654    * last block to read.
655    *
656    * @param bufferGiven buffer to use with the limit size for the block array
657    *
658    * @return the resulting DataBlock (even empty or partial)
659    *
660    * @throws FileTransferException
661    * @throws FileEndOfTransferException
662    */
663   private synchronized DataBlock getByteBlock(final byte[] bufferGiven)
664       throws FileTransferException, FileEndOfTransferException {
665     if (!isReady) {
666       throw new FileTransferException(NO_FILE_IS_READY);
667     }
668     final int sizeblock = bufferGiven.length;
669     if (fileInputStream == null) {
670       fileInputStream = getFileInputStream();
671       if (fileInputStream == null) {
672         throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
673       }
674       reusableBytes = bufferGiven;
675     }
676     int sizeout = 0;
677     while (sizeout < sizeblock) {
678       try {
679         final int sizeread =
680             fileInputStream.read(reusableBytes, sizeout, sizeblock - sizeout);
681         if (sizeread <= 0) {
682           break;
683         }
684         sizeout += sizeread;
685       } catch (final IOException e) {
686         logger.error(ERROR_DURING_GET + " {}", e.getMessage());
687         try {
688           closeFile();
689         } catch (final CommandAbstractException ignored) {
690           // nothing
691         }
692         throw new FileTransferException(INTERNAL_ERROR_FILE_IS_NOT_READY);
693       }
694     }
695     if (sizeout <= 0) {
696       try {
697         closeFile();
698       } catch (final CommandAbstractException ignored) {
699         // nothing
700       }
701       isReady = false;
702       throw new FileEndOfTransferException("End of file");
703     }
704     position += sizeout;
705     final DataBlock dataBlock = new DataBlock();
706     dataBlock.setBlock(reusableBytes, sizeout);
707     if (sizeout < sizeblock) {// last block
708       dataBlock.setEOF(true);
709       try {
710         closeFile();
711       } catch (final CommandAbstractException ignored) {
712         // nothing
713       }
714       isReady = false;
715     }
716     return dataBlock;
717   }
718 
719   protected FileInputStream getFileInputStream() {
720     if (!isReady) {
721       return null;
722     }
723     try {
724       if (currentRealFile == null) {
725         currentRealFile = getFileFromPath(currentFile);
726       }
727     } catch (final CommandAbstractException e1) {
728       return null;
729     }
730     @SuppressWarnings("resource")
731     FileInputStream fileInputStreamTemp = null;
732     try {
733       fileInputStreamTemp = new FileInputStream(currentRealFile);//NOSONAR
734       if (position != 0) {
735         final long read = fileInputStreamTemp.skip(position);
736         if (read != position) {
737           logger.warn("Cannot ensure position: {} while is {}", position, read);
738         }
739       }
740     } catch (final FileNotFoundException e) {
741       FileUtils.close(fileInputStreamTemp);
742       logger.error("File not found in getFileInputStream: {}", e.getMessage());
743       return null;
744     } catch (final IOException e) {
745       FileUtils.close(fileInputStreamTemp);
746       logger.error("Change position in getFileInputStream: {}", e.getMessage());
747       return null;
748     }
749     return fileInputStreamTemp;
750   }
751 
752   /**
753    * Returns the RandomAccessFile in Out mode associated with the current
754    * file.
755    *
756    * @return the RandomAccessFile (OUT="rw")
757    */
758   protected RandomAccessFile getRandomFile() {
759     if (!isReady) {
760       return null;
761     }
762     try {
763       if (currentRealFile == null) {
764         currentRealFile = getFileFromPath(currentFile);
765       }
766     } catch (final CommandAbstractException e1) {
767       return null;
768     }
769     final RandomAccessFile raf;
770     try {
771       raf = new RandomAccessFile(currentRealFile, "rw");//NOSONAR
772       raf.seek(position);
773     } catch (final FileNotFoundException e) {
774       logger.error("File not found in getRandomFile: {}", e.getMessage());
775       return null;
776     } catch (final IOException e) {
777       logger.error("Change position in getRandomFile: {}", e.getMessage());
778       return null;
779     }
780     return raf;
781   }
782 
783   /**
784    * Returns the FileOutputStream in Out mode associated with the current
785    * file.
786    *
787    * @param append True if the FileOutputStream should be in append
788    *     mode
789    *
790    * @return the FileOutputStream (OUT)
791    */
792   protected FileOutputStream getFileOutputStream(final boolean append) {
793     if (!isReady) {
794       return null;
795     }
796     try {
797       if (currentRealFile == null) {
798         currentRealFile = getFileFromPath(currentFile);
799       }
800     } catch (final CommandAbstractException e1) {
801       return null;
802     }
803     if (position > 0) {
804       if (currentRealFile.length() < position) {
805         logger.error(
806             "Cannot Change position in getFileOutputStream: file is smaller than required position");
807         return null;
808       }
809       final RandomAccessFile raf = getRandomFile();
810       try {
811         raf.setLength(position);
812         FileUtils.close(raf);
813       } catch (final IOException e) {
814         logger.error("Change position in getFileOutputStream: {}",
815                      e.getMessage());
816         return null;
817       }
818       if (logger.isDebugEnabled()) {
819         logger.debug("New size: {}:{}", currentRealFile.length(), position);
820       }
821     }
822     final FileOutputStream fos;
823     try {
824       fos = new FileOutputStream(currentRealFile, append);
825     } catch (final FileNotFoundException e) {
826       logger.error("File not found in getRandomFile: {}", e.getMessage());
827       return null;
828     }
829     return fos;
830   }
831 }