View Javadoc
1   /*
2    * Copyright (C) 2005-2015 Schlichtherle IT Services.
3    * All rights reserved. Use is subject to license terms.
4    */
5   package net.java.truevfs.comp.tardriver;
6   
7   import edu.umd.cs.findbugs.annotations.CleanupObligation;
8   import edu.umd.cs.findbugs.annotations.CreatesObligation;
9   import edu.umd.cs.findbugs.annotations.DischargesObligation;
10  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
11  import net.java.truecommons.cio.*;
12  import net.java.truecommons.io.DecoratingOutputStream;
13  import net.java.truecommons.io.DisconnectingOutputStream;
14  import net.java.truecommons.io.Sink;
15  import net.java.truecommons.io.Streams;
16  import net.java.truecommons.shed.SuppressedExceptionBuilder;
17  import net.java.truevfs.kernel.spec.FsModel;
18  import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
19  
20  import javax.annotation.CheckForNull;
21  import javax.annotation.concurrent.NotThreadSafe;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.util.*;
26  
27  import static net.java.truecommons.cio.Entry.Size.DATA;
28  import static net.java.truecommons.cio.Entry.UNKNOWN;
29  import static net.java.truecommons.shed.HashMaps.OVERHEAD_SIZE;
30  import static net.java.truecommons.shed.HashMaps.initialCapacity;
31  import static org.apache.commons.compress.archivers.tar.TarConstants.DEFAULT_BLKSIZE;
32  import static org.apache.commons.compress.archivers.tar.TarConstants.DEFAULT_RCDSIZE;
33  
34  /**
35   * An output service for writing TAR files.
36   * This output service can only write one entry concurrently.
37   * <p>
38   * Because the TAR file format needs to know each entry's length in advance,
39   * entries from an unknown source are actually written to temp files and copied
40   * to the underlying {@link TarArchiveOutputStream} upon a call to their
41   * {@link OutputStream#close} method.
42   * Note that this implies that the {@code close()} method may fail with
43   * an {@link IOException}.
44   * <p>
45   * If the size of an entry is known in advance it's directly written to the
46   * underlying {@code TarArchiveOutputStream} instead.
47   *
48   * @see    TarInputService
49   * @author Christian Schlichtherle
50   */
51  @NotThreadSafe
52  public final class TarOutputService
53  implements OutputService<TarDriverEntry> {
54  
55      /** Maps entry names to tar entries [String -> TarDriverEntry]. */
56      private final Map<String, TarDriverEntry>
57              entries = new LinkedHashMap<>(initialCapacity(OVERHEAD_SIZE));
58  
59      private final TarArchiveOutputStream taos;
60      private final TarDriver driver;
61      private boolean busy;
62  
63      @CreatesObligation
64      public TarOutputService(
65              final FsModel model,
66              final Sink sink,
67              final TarDriver driver)
68      throws IOException {
69          Objects.requireNonNull(model);
70          this.driver = Objects.requireNonNull(driver);
71          final OutputStream out = sink.stream();
72          try {
73              final TarArchiveOutputStream
74                      taos = this.taos = new TarArchiveOutputStream(out,
75                          DEFAULT_BLKSIZE, DEFAULT_RCDSIZE, driver.getEncoding());
76              taos.setAddPaxHeadersForNonAsciiNames(driver.getAddPaxHeaderForNonAsciiNames());
77              taos.setLongFileMode(driver.getLongFileMode());
78              taos.setBigNumberMode(driver.getBigNumberMode());
79          } catch (final Throwable ex) {
80              try {
81                  out.close();
82              } catch (final Throwable ex2) {
83                  ex.addSuppressed(ex2);
84              }
85              throw ex;
86          }
87      }
88  
89      private IoBufferPool getPool() {
90          return driver.getPool();
91      }
92  
93      @Override
94      public int size() {
95          return entries.size();
96      }
97  
98      @Override
99      public Iterator<TarDriverEntry> iterator() {
100         return Collections.unmodifiableCollection(entries.values()).iterator();
101     }
102 
103     @Override
104     public @CheckForNull TarDriverEntry entry(String name) {
105         return entries.get(name);
106     }
107 
108     @Override
109     public OutputSocket<TarDriverEntry> output(final TarDriverEntry local) {
110         Objects.requireNonNull(local);
111         final class Output extends AbstractOutputSocket<TarDriverEntry> {
112             @Override
113             public TarDriverEntry target() {
114                 return local;
115             }
116 
117             @Override
118             public OutputStream stream(final InputSocket<? extends Entry> peer)
119             throws IOException {
120                 if (isBusy()) throw new OutputBusyException(local.getName());
121                 if (local.isDirectory()) {
122                     updateProperties(local, DirectoryTemplate.INSTANCE);
123                     return new EntryOutputStream(local);
124                 }
125                 updateProperties(local, target(peer));
126                 return UNKNOWN == local.getSize()
127                         ? new BufferedEntryOutputStream(local)
128                         : new EntryOutputStream(local);
129             }
130         } // Output
131         return new Output();
132     }
133 
134     void updateProperties(
135             final TarDriverEntry local,
136             final @CheckForNull Entry peer) {
137         if (UNKNOWN == local.getModTime().getTime())
138             local.setModTime(System.currentTimeMillis());
139         if (null != peer)
140             if (UNKNOWN == local.getSize())
141                 local.setSize(peer.getSize(DATA));
142     }
143 
144     private static final class DirectoryTemplate implements Entry {
145         static final DirectoryTemplate INSTANCE = new DirectoryTemplate();
146 
147         @Override
148         public String getName() { return "/"; }
149 
150         @Override
151         public long getSize(Size type) { return 0; }
152 
153         @Override
154         public long getTime(Access type) { return UNKNOWN; }
155 
156         @Override
157         @SuppressFBWarnings("NP_BOOLEAN_RETURN_NULL")
158         public Boolean isPermitted(Access type, Entity entity) { return null; }
159     } // DirectoryTemplate
160 
161     /**
162      * Returns whether this TAR output service is busy writing an archive entry
163      * or not.
164      *
165      * @return Whether this TAR output service is busy writing an archive entry
166      *         or not.
167      */
168     private boolean isBusy() {
169         return busy;
170     }
171 
172     @Override
173     public void close() throws IOException {
174         taos.close();
175     }
176 
177     /**
178      * This entry output stream writes directly to the subclass.
179      * It can only be used if this output stream is not currently busy
180      * writing another entry and the entry holds enough information to
181      * write the entry header.
182      * These preconditions are checked by {@link #output(TarDriverEntry)}.
183      */
184     @CleanupObligation
185     private final class EntryOutputStream extends DisconnectingOutputStream {
186         boolean closed;
187 
188         @CreatesObligation
189         EntryOutputStream(final TarDriverEntry local)
190         throws IOException {
191             super(taos);
192             taos.putArchiveEntry(local);
193             entries.put(local.getName(), local);
194             busy = true;
195         }
196 
197         @Override
198         public boolean isOpen() {
199             return !closed;
200         }
201 
202         @Override
203         @DischargesObligation
204         public void close() throws IOException {
205             if (closed) return;
206             closed = true;
207             busy = false;
208             taos.closeArchiveEntry();
209         }
210     } // EntryOutputStream
211 
212     /**
213      * This entry output stream writes the entry to an I/O buffer.
214      * When the stream is closed, the temporary file is then copied to this
215      * output stream and finally deleted.
216      */
217     @CleanupObligation
218     private final class BufferedEntryOutputStream
219     extends DecoratingOutputStream {
220 
221         final IoBuffer buffer;
222         final TarDriverEntry local;
223         boolean closed;
224 
225         @CreatesObligation
226         BufferedEntryOutputStream(final TarDriverEntry local)
227         throws IOException {
228             this.local = local;
229             final IoBuffer buffer = this.buffer = getPool().allocate();
230             try {
231                 this.out = buffer.output().stream(null);
232             } catch (final Throwable ex) {
233                 try {
234                     buffer.release();
235                 } catch (final Throwable ex2) {
236                     ex.addSuppressed(ex2);
237                 }
238                 throw ex;
239             }
240             entries.put(local.getName(), local);
241             busy = true;
242         }
243 
244         @Override
245         @DischargesObligation
246         public void close() throws IOException {
247             if (closed) return;
248             closed = true;
249             busy = false;
250             out.close();
251             updateProperties(local, buffer);
252             storeBuffer();
253         }
254 
255         @SuppressWarnings("ThrowFromFinallyBlock")
256         void storeBuffer() throws IOException {
257             final IoBuffer buffer = this.buffer;
258             Throwable t1 = null;
259             try (final InputStream in = buffer.input().stream(null)) {
260                 final TarArchiveOutputStream taos = TarOutputService.this.taos;
261                 taos.putArchiveEntry(local);
262                 Streams.cat(in, taos);
263                 taos.closeArchiveEntry();
264             } catch (final Throwable t2) {
265                 t1 = t2;
266                 throw t2;
267             } finally {
268                 try {
269                     buffer.release();
270                 } catch (final Throwable t2) {
271                     if (null == t1) throw t2;
272                     t1.addSuppressed(t2);
273                 }
274             }
275         }
276     } // BufferedEntryOutputStream
277 }