001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.tar;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.nio.file.Files;
023import java.util.Collection;
024import java.util.HashMap;
025import java.util.Map;
026import java.util.Objects;
027import java.util.zip.GZIPInputStream;
028
029import org.apache.commons.compress.archivers.ArchiveEntry;
030import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
031import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.apache.commons.vfs2.Capability;
035import org.apache.commons.vfs2.FileName;
036import org.apache.commons.vfs2.FileNotFoundException;
037import org.apache.commons.vfs2.FileObject;
038import org.apache.commons.vfs2.FileSystemException;
039import org.apache.commons.vfs2.FileSystemOptions;
040import org.apache.commons.vfs2.Selectors;
041import org.apache.commons.vfs2.VfsLog;
042import org.apache.commons.vfs2.provider.AbstractFileName;
043import org.apache.commons.vfs2.provider.AbstractFileSystem;
044import org.apache.commons.vfs2.provider.UriParser;
045import org.apache.commons.vfs2.provider.bzip2.Bzip2FileObject;
046
047/**
048 * A read-only file system for Tar files.
049 */
050public class TarFileSystem extends AbstractFileSystem {
051    private static final Log LOG = LogFactory.getLog(TarFileSystem.class);
052
053    private static final char[] ENC = {'!'};
054
055    private final File file;
056
057    private TarArchiveInputStream tarFile;
058
059    /**
060     * Cache doesn't need to be synchronized since it is read-only.
061     */
062    private final Map<FileName, FileObject> cache = new HashMap<>();
063
064    /**
065     * Constructs a new instance.
066     *
067     * @param rootName The root file name of this file system.
068     * @param parentLayer The parent layer of this file system.
069     * @param fileSystemOptions Options to build this file system.
070     * @throws FileSystemException if a file system error occurs.
071     */
072    protected TarFileSystem(final AbstractFileName rootName, final FileObject parentLayer,
073        final FileSystemOptions fileSystemOptions) throws FileSystemException {
074        super(rootName, parentLayer, fileSystemOptions);
075
076        // Make a local copy of the file
077        file = parentLayer.getFileSystem().replicateFile(parentLayer, Selectors.SELECT_SELF);
078
079        // Open the Tar file
080        if (!file.exists()) {
081            // Don't need to do anything
082            tarFile = null;
083        }
084
085        // tarFile = createTarFile(this.file);
086    }
087
088    /**
089     * Returns the capabilities of this file system.
090     */
091    @Override
092    protected void addCapabilities(final Collection<Capability> caps) {
093        caps.addAll(TarFileProvider.capabilities);
094    }
095
096    /**
097     * Creates a file object.
098     */
099    @Override
100    protected FileObject createFile(final AbstractFileName name) throws FileSystemException {
101        // This is only called for files which do not exist in the Tar file
102        return new TarFileObject(name, null, this, false);
103    }
104
105    /**
106     * Creates a new TarArchiveInputStream.
107     *
108     * @param file the file.
109     * @return a new TarArchiveInputStream.
110     * @throws FileSystemException if a file system error occurs.
111     */
112    protected TarArchiveInputStream createTarFile(final File file) throws FileSystemException {
113        try {
114            if ("tgz".equalsIgnoreCase(getRootName().getScheme())) {
115                return new TarArchiveInputStream(new GZIPInputStream(Files.newInputStream(file.toPath())));
116            }
117            if ("tbz2".equalsIgnoreCase(getRootName().getScheme())) {
118                return new TarArchiveInputStream(
119                    Bzip2FileObject.wrapInputStream(file.getAbsolutePath(), Files.newInputStream(file.toPath())));
120            }
121            return new TarArchiveInputStream(Files.newInputStream(file.toPath()));
122        } catch (final IOException ioe) {
123            throw new FileSystemException("vfs.provider.tar/open-tar-file.error", file, ioe);
124        }
125    }
126
127    /**
128     * Creates a new TarFileObject.
129     *
130     * @param fileName the file name.
131     * @param entry the archive entry.
132     * @return a new TarFileObject.
133     */
134    protected TarFileObject createTarFileObject(final AbstractFileName fileName, final TarArchiveEntry entry) {
135        return new TarFileObject(fileName, entry, this, true);
136    }
137
138    @Override
139    protected void doCloseCommunicationLink() {
140        // Release the tar file
141        try {
142            if (tarFile != null) {
143                tarFile.close();
144                tarFile = null;
145            }
146        } catch (final IOException e) {
147            // getLogger().warn("vfs.provider.tar/close-tar-file.error :" + file, e);
148            VfsLog.warn(getLogger(), LOG, "vfs.provider.tar/close-tar-file.error :" + file, e);
149        }
150    }
151
152    /**
153     * Returns a cached file.
154     */
155    @Override
156    protected FileObject getFileFromCache(final FileName name) {
157        return cache.get(name);
158    }
159
160    /**
161     * Returns the input stream for the given entry.
162     *
163     * @param entry The entry to seek.
164     * @return the input stream for the given entry.
165     * @throws FileSystemException If an I/O error occurs.
166     */
167    public InputStream getInputStream(final TarArchiveEntry entry) throws FileSystemException {
168        Objects.requireNonNull(entry, "entry");
169        resetTarFile();
170        try {
171            ArchiveEntry next;
172            while ((next = tarFile.getNextEntry()) != null) {
173                if (next.equals(entry)) {
174                    return tarFile;
175                }
176            }
177            throw new FileNotFoundException(entry.toString());
178        } catch (final IOException e) {
179            throw new FileSystemException(e);
180        }
181    }
182
183    /**
184     * Gets the TarArchiveInputStream.
185     *
186     * @return the TarArchiveInputStream.
187     * @throws FileSystemException if a file system error occurs.
188     */
189    protected TarArchiveInputStream getTarFile() throws FileSystemException {
190        if (tarFile == null && file.exists()) {
191            recreateTarFile();
192        }
193        return tarFile;
194    }
195
196    @Override
197    public void init() throws FileSystemException {
198        super.init();
199
200        // Build the index
201        try {
202            TarArchiveEntry entry;
203            while ((entry = getTarFile().getNextTarEntry()) != null) {
204                final AbstractFileName name = (AbstractFileName) getFileSystemManager().resolveName(getRootName(),
205                    UriParser.encode(entry.getName(), ENC));
206
207                // Create the file
208                TarFileObject fileObj;
209                if (entry.isDirectory() && getFileFromCache(name) != null) {
210                    fileObj = (TarFileObject) getFileFromCache(name);
211                    fileObj.setTarEntry(entry);
212                    continue;
213                }
214
215                fileObj = createTarFileObject(name, entry);
216                putFileToCache(fileObj);
217
218                // Make sure all ancestors exist
219                // TODO - create these on demand
220                TarFileObject parent = null;
221                for (AbstractFileName parentName = (AbstractFileName) name
222                    .getParent(); parentName != null; fileObj = parent, parentName = (AbstractFileName) parentName
223                        .getParent()) {
224                    // Locate the parent
225                    parent = (TarFileObject) getFileFromCache(parentName);
226                    if (parent == null) {
227                        parent = createTarFileObject(parentName, null);
228                        putFileToCache(parent);
229                    }
230
231                    // Attach child to parent
232                    parent.attachChild(fileObj.getName());
233                }
234            }
235        } catch (final IOException e) {
236            throw new FileSystemException(e);
237        } finally {
238            closeCommunicationLink();
239        }
240    }
241
242    /**
243     * Adds a file object to the cache.
244     */
245    @Override
246    protected void putFileToCache(final FileObject file) {
247        cache.put(file.getName(), file);
248    }
249
250    /**
251     * will be called after all file-objects closed their streams. protected void notifyAllStreamsClosed() {
252     * closeCommunicationLink(); }
253     */
254    private void recreateTarFile() throws FileSystemException {
255        if (tarFile != null) {
256            try {
257                tarFile.close();
258            } catch (final IOException e) {
259                throw new FileSystemException("vfs.provider.tar/close-tar-file.error", file, e);
260            }
261            tarFile = null;
262        }
263        tarFile = createTarFile(file);
264    }
265
266    /**
267     * remove a cached file.
268     */
269    @Override
270    protected void removeFileFromCache(final FileName name) {
271        cache.remove(name);
272    }
273
274    /**
275     * Resets the tar file.
276     *
277     * @throws FileSystemException if a file system error occurs.
278     */
279    protected void resetTarFile() throws FileSystemException {
280        // Reading specific entries requires skipping through the tar file from the beginning
281        // Not especially elegant, but we don't have the ability to seek to specific positions
282        // with an input stream.
283        if (file.exists()) {
284            recreateTarFile();
285        }
286    }
287}