This project has retired. For details please refer to its Attic page.
Source code
001package org.apache.archiva.repository.storage;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.commons.lang3.StringUtils;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.nio.channels.FileChannel;
030import java.nio.channels.ReadableByteChannel;
031import java.nio.channels.WritableByteChannel;
032import java.nio.file.*;
033import java.nio.file.attribute.*;
034import java.time.Instant;
035import java.util.ArrayList;
036import java.util.Collections;
037import java.util.List;
038import java.util.Set;
039import java.util.stream.Collectors;
040
041/**
042 * Implementation of an asset that is stored on the filesystem.
043 * <p>
044 * The implementation does not check the given paths. Caller should normalize the asset path
045 * and check, if the base path is a parent of the resulting path.
046 * <p>
047 * The file must not exist for all operations.
048 *
049 * @author Martin Stockhammer <martin_s@apache.org>
050 */
051public class FilesystemAsset implements StorageAsset, Comparable {
052
053    private final static Logger log = LoggerFactory.getLogger(FilesystemAsset.class);
054
055    private final Path basePath;
056    private final Path assetPath;
057    private final String relativePath;
058
059    public static final String DEFAULT_POSIX_FILE_PERMS = "rw-rw----";
060    public static final String DEFAULT_POSIX_DIR_PERMS = "rwxrwx---";
061
062    public static final Set<PosixFilePermission> DEFAULT_POSIX_FILE_PERMISSIONS;
063    public static final Set<PosixFilePermission> DEFAULT_POSIX_DIR_PERMISSIONS;
064
065    public static final AclEntryPermission[] DEFAULT_ACL_FILE_PERMISSIONS = new AclEntryPermission[]{
066            AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_ACL,
067            AclEntryPermission.WRITE_ATTRIBUTES, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA
068    };
069
070    public static final AclEntryPermission[] DEFAULT_ACL_DIR_PERMISSIONS = new AclEntryPermission[]{
071            AclEntryPermission.ADD_FILE, AclEntryPermission.ADD_SUBDIRECTORY, AclEntryPermission.DELETE_CHILD,
072            AclEntryPermission.DELETE, AclEntryPermission.READ_ACL, AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_DATA, AclEntryPermission.WRITE_ACL,
073            AclEntryPermission.WRITE_ATTRIBUTES, AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA
074    };
075
076    static {
077
078        DEFAULT_POSIX_FILE_PERMISSIONS = PosixFilePermissions.fromString(DEFAULT_POSIX_FILE_PERMS);
079        DEFAULT_POSIX_DIR_PERMISSIONS = PosixFilePermissions.fromString(DEFAULT_POSIX_DIR_PERMS);
080    }
081
082    Set<PosixFilePermission> defaultPosixFilePermissions = DEFAULT_POSIX_FILE_PERMISSIONS;
083    Set<PosixFilePermission> defaultPosixDirectoryPermissions = DEFAULT_POSIX_DIR_PERMISSIONS;
084
085    List<AclEntry> defaultFileAcls;
086    List<AclEntry> defaultDirectoryAcls;
087
088    boolean supportsAcl = false;
089    boolean supportsPosix = false;
090    final boolean setPermissionsForNew;
091    final RepositoryStorage storage;
092
093    boolean directoryHint = false;
094
095    private static final OpenOption[] REPLACE_OPTIONS = new OpenOption[]{StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE};
096    private static final OpenOption[] APPEND_OPTIONS = new OpenOption[]{StandardOpenOption.APPEND};
097
098
099    FilesystemAsset(RepositoryStorage storage, String path, Path assetPath, Path basePath) {
100        this.assetPath = assetPath;
101        this.relativePath = normalizePath(path);
102        this.setPermissionsForNew=false;
103        this.basePath = basePath;
104        this.storage = storage;
105        init();
106    }
107
108    /**
109     * Creates an asset for the given path. The given paths are not checked.
110     * The base path should be an absolute path.
111     *
112     * @param path The logical path for the asset relative to the repository.
113     * @param assetPath The asset path.
114     */
115    public FilesystemAsset(RepositoryStorage storage, String path, Path assetPath) {
116        this.assetPath = assetPath;
117        this.relativePath = normalizePath(path);
118        this.setPermissionsForNew = false;
119        this.basePath = null;
120        this.storage = storage;
121        // The base directory is always a directory
122        if ("".equals(path) || "/".equals(path)) {
123            this.directoryHint = true;
124        }
125        init();
126    }
127
128    /**
129     * Creates an asset for the given path. The given paths are not checked.
130     * The base path should be an absolute path.
131     *
132     * @param path The logical path for the asset relative to the repository
133     * @param assetPath The asset path.
134     * @param directory This is only relevant, if the represented file or directory does not exist yet and
135     *                  is a hint.
136     */
137    public FilesystemAsset(RepositoryStorage storage, String path, Path assetPath, Path basePath, boolean directory) {
138        this.assetPath = assetPath;
139        this.relativePath = normalizePath(path);
140        this.directoryHint = directory;
141        this.setPermissionsForNew = false;
142        this.basePath = basePath;
143        this.storage = storage;
144        init();
145    }
146
147    /**
148     * Creates an asset for the given path. The given paths are not checked.
149     * The base path should be an absolute path.
150     *
151     * @param path The logical path for the asset relative to the repository
152     * @param assetPath The asset path.
153     * @param directory This is only relevant, if the represented file or directory does not exist yet and
154     *                  is a hint.
155     */
156    public FilesystemAsset(RepositoryStorage storage, String path, Path assetPath, Path basePath, boolean directory, boolean setPermissionsForNew) {
157        this.assetPath = assetPath;
158        this.relativePath = normalizePath(path);
159        this.directoryHint = directory;
160        this.setPermissionsForNew = setPermissionsForNew;
161        this.basePath = basePath;
162        this.storage = storage;
163        init();
164    }
165
166    private String normalizePath(final String path) {
167        if (!path.startsWith("/")) {
168            return "/"+path;
169        } else {
170            String tmpPath = path;
171            while (tmpPath.startsWith("//")) {
172                tmpPath = tmpPath.substring(1);
173            }
174            return tmpPath;
175        }
176    }
177
178    private void init() {
179
180        if (setPermissionsForNew) {
181            try {
182                supportsAcl = Files.getFileStore(assetPath.getRoot()).supportsFileAttributeView(AclFileAttributeView.class);
183            } catch (IOException e) {
184                log.error("Could not check filesystem capabilities {}", e.getMessage());
185            }
186            try {
187                supportsPosix = Files.getFileStore(assetPath.getRoot()).supportsFileAttributeView(PosixFileAttributeView.class);
188            } catch (IOException e) {
189                log.error("Could not check filesystem capabilities {}", e.getMessage());
190            }
191
192            if (supportsAcl) {
193                AclFileAttributeView aclView = Files.getFileAttributeView(assetPath.getParent(), AclFileAttributeView.class);
194                UserPrincipal owner = null;
195                try {
196                    owner = aclView.getOwner();
197                    setDefaultFileAcls(processPermissions(owner, DEFAULT_ACL_FILE_PERMISSIONS));
198                    setDefaultDirectoryAcls(processPermissions(owner, DEFAULT_ACL_DIR_PERMISSIONS));
199
200                } catch (IOException e) {
201                    supportsAcl = false;
202                }
203
204
205            }
206        }
207    }
208
209    private List<AclEntry> processPermissions(UserPrincipal owner, AclEntryPermission[] defaultAclFilePermissions) {
210        AclEntry.Builder aclBuilder = AclEntry.newBuilder();
211        aclBuilder.setPermissions(defaultAclFilePermissions);
212        aclBuilder.setType(AclEntryType.ALLOW);
213        aclBuilder.setPrincipal(owner);
214        ArrayList<AclEntry> aclList = new ArrayList<>();
215        aclList.add(aclBuilder.build());
216        return aclList;
217    }
218
219
220    @Override
221    public RepositoryStorage getStorage( )
222    {
223        return storage;
224    }
225
226    @Override
227    public String getPath() {
228        return relativePath;
229    }
230
231    @Override
232    public String getName() {
233        return assetPath.getFileName().toString();
234    }
235
236    @Override
237    public Instant getModificationTime() {
238        try {
239            return Files.getLastModifiedTime(assetPath).toInstant();
240        } catch (IOException e) {
241            log.error("Could not read modification time of {}", assetPath);
242            return Instant.now();
243        }
244    }
245
246    /**
247     * Returns true, if the path of this asset points to a directory
248     *
249     * @return
250     */
251    @Override
252    public boolean isContainer() {
253        if (Files.exists(assetPath)) {
254            return Files.isDirectory(assetPath);
255        } else {
256            return directoryHint;
257        }
258    }
259
260    /**
261     * Returns the list of directory entries, if this asset represents a directory.
262     * Otherwise a empty list will be returned.
263     *
264     * @return The list of entries in the directory, if it exists.
265     */
266    @Override
267    public List<StorageAsset> list() {
268        try {
269            return Files.list(assetPath).map(p -> new FilesystemAsset(storage, relativePath + "/" + p.getFileName().toString(), assetPath.resolve(p)))
270                    .collect(Collectors.toList());
271        } catch (IOException e) {
272            return Collections.EMPTY_LIST;
273        }
274    }
275
276    /**
277     * Returns the size of the represented file. If it cannot be determined, -1 is returned.
278     *
279     * @return
280     */
281    @Override
282    public long getSize() {
283        try {
284            return Files.size(assetPath);
285        } catch (IOException e) {
286            return -1;
287        }
288    }
289
290    /**
291     * Returns a input stream to the underlying file, if it exists. The caller has to make sure, that
292     * the stream is closed after it was used.
293     *
294     * @return
295     * @throws IOException
296     */
297    @Override
298    public InputStream getReadStream() throws IOException {
299        if (isContainer()) {
300            throw new IOException("Can not create input stream for container");
301        }
302        return Files.newInputStream(assetPath);
303    }
304
305    @Override
306    public ReadableByteChannel getReadChannel( ) throws IOException
307    {
308        return FileChannel.open( assetPath, StandardOpenOption.READ );
309    }
310
311    private OpenOption[] getOpenOptions(boolean replace) {
312        return replace ? REPLACE_OPTIONS : APPEND_OPTIONS;
313    }
314
315    @Override
316    public OutputStream getWriteStream( boolean replace) throws IOException {
317        OpenOption[] options = getOpenOptions( replace );
318        if (!Files.exists( assetPath )) {
319            create();
320        }
321        return Files.newOutputStream(assetPath, options);
322    }
323
324    @Override
325    public WritableByteChannel getWriteChannel( boolean replace ) throws IOException
326    {
327        OpenOption[] options = getOpenOptions( replace );
328        return FileChannel.open( assetPath, options );
329    }
330
331    @Override
332    public boolean replaceDataFromFile( Path newData) throws IOException {
333        final boolean createNew = !Files.exists(assetPath);
334        Path backup = null;
335        if (!createNew) {
336            backup = findBackupFile(assetPath);
337        }
338        try {
339            if (!createNew) {
340                Files.move(assetPath, backup);
341            }
342            Files.move(newData, assetPath, StandardCopyOption.REPLACE_EXISTING);
343            applyDefaultPermissions(assetPath);
344            return true;
345        } catch (IOException e) {
346            log.error("Could not overwrite file {}", assetPath);
347            // Revert if possible
348            if (backup != null && Files.exists(backup)) {
349                Files.move(backup, assetPath, StandardCopyOption.REPLACE_EXISTING);
350            }
351            throw e;
352        } finally {
353            if (backup != null) {
354                try {
355                    Files.deleteIfExists(backup);
356                } catch (IOException e) {
357                    log.error("Could not delete backup file {}", backup);
358                }
359            }
360        }
361
362    }
363
364    private void applyDefaultPermissions(Path filePath) {
365        try {
366            if (supportsPosix) {
367                Set<PosixFilePermission> perms;
368                if (Files.isDirectory(filePath)) {
369                    perms = defaultPosixFilePermissions;
370                } else {
371                    perms = defaultPosixDirectoryPermissions;
372                }
373                Files.setPosixFilePermissions(filePath, perms);
374            } else if (supportsAcl) {
375                List<AclEntry> perms;
376                if (Files.isDirectory(filePath)) {
377                    perms = getDefaultDirectoryAcls();
378                } else {
379                    perms = getDefaultFileAcls();
380                }
381                AclFileAttributeView aclAttr = Files.getFileAttributeView(filePath, AclFileAttributeView.class);
382                aclAttr.setAcl(perms);
383            }
384        } catch (IOException e) {
385            log.error("Could not set permissions for {}: {}", filePath, e.getMessage());
386        }
387    }
388
389    private Path findBackupFile(Path file) {
390        String ext = ".bak";
391        Path backupPath = file.getParent().resolve(file.getFileName().toString() + ext);
392        int idx = 0;
393        while (Files.exists(backupPath)) {
394            backupPath = file.getParent().resolve(file.getFileName().toString() + ext + idx++);
395        }
396        return backupPath;
397    }
398
399    @Override
400    public boolean exists() {
401        return Files.exists(assetPath);
402    }
403
404    @Override
405    public Path getFilePath() throws UnsupportedOperationException {
406        return assetPath;
407    }
408
409    @Override
410    public boolean isFileBased( )
411    {
412        return true;
413    }
414
415    @Override
416    public boolean hasParent( )
417    {
418        if (basePath!=null && assetPath.equals(basePath)) {
419                return false;
420        }
421        return assetPath.getParent()!=null;
422    }
423
424    @Override
425    public StorageAsset getParent( )
426    {
427        Path parentPath;
428        if (basePath!=null && assetPath.equals( basePath )) {
429            parentPath=null;
430        } else
431        {
432            parentPath = assetPath.getParent( );
433        }
434        String relativeParent = StringUtils.substringBeforeLast( relativePath,"/");
435        if (parentPath!=null) {
436            return new FilesystemAsset(storage, relativeParent, parentPath, basePath, true, setPermissionsForNew );
437        } else {
438            return null;
439        }
440    }
441
442    @Override
443    public StorageAsset resolve(String toPath) {
444        return storage.getAsset(this.getPath()+"/"+toPath);
445    }
446
447
448    public void setDefaultFileAcls(List<AclEntry> acl) {
449        defaultFileAcls = acl;
450    }
451
452    public List<AclEntry> getDefaultFileAcls() {
453        return defaultFileAcls;
454    }
455
456    public void setDefaultPosixFilePermissions(Set<PosixFilePermission> perms) {
457        defaultPosixFilePermissions = perms;
458    }
459
460    public Set<PosixFilePermission> getDefaultPosixFilePermissions() {
461        return defaultPosixFilePermissions;
462    }
463
464    public void setDefaultDirectoryAcls(List<AclEntry> acl) {
465        defaultDirectoryAcls = acl;
466    }
467
468    public List<AclEntry> getDefaultDirectoryAcls() {
469        return defaultDirectoryAcls;
470    }
471
472    public void setDefaultPosixDirectoryPermissions(Set<PosixFilePermission> perms) {
473        defaultPosixDirectoryPermissions = perms;
474    }
475
476    public Set<PosixFilePermission> getDefaultPosixDirectoryPermissions() {
477        return defaultPosixDirectoryPermissions;
478    }
479
480    @Override
481    public void create() throws IOException {
482        if (!Files.exists(assetPath)) {
483            if (directoryHint) {
484                Files.createDirectories(assetPath);
485            } else {
486                if (!Files.exists( assetPath.getParent() )) {
487                    Files.createDirectories( assetPath.getParent( ) );
488                }
489                Files.createFile(assetPath);
490            }
491            if (setPermissionsForNew) {
492                applyDefaultPermissions(assetPath);
493            }
494        }
495    }
496
497    @Override
498    public String toString() {
499        return relativePath+":"+assetPath;
500    }
501
502    @Override
503    public int compareTo(Object o) {
504        if (o instanceof FilesystemAsset) {
505            if (this.getPath()!=null) {
506                return this.getPath().compareTo(((FilesystemAsset) o).getPath());
507            } else {
508                return 0;
509            }
510        }
511        return 0;
512    }
513}