/* * Universal Media Server, for streaming any media to DLNA * compatible renderers based on the http://www.ps3mediaserver.org. * Copyright (C) 2012 UMS developers. * * This program is a free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.util; import com.sun.jna.Platform; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.AccessDeniedException; import java.nio.file.AccessMode; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An object that encapsulates file permissions for a <code>File</code> object. * If there are insufficient permission to read the <code>File</code> object's * permissions, all permissions will return false (even though some of them * might be true) and no <code>Exception</code> will be thrown. * This is due to limitations in the underlying methods. * * @author Nadahar * @threadsafe */ public class FilePermissions { private final File file; private final Path path; private Boolean read = null; private Boolean write = null; private Boolean execute = null; private final boolean folder; private String lastCause = null; private static final Logger LOGGER = LoggerFactory.getLogger(FilePermissions.class); public FilePermissions(File file) throws FileNotFoundException { if (file == null) { throw new IllegalArgumentException("File argument cannot be null"); } /* Go via .getAbsoluteFile() to work around a bug where new File("") * (current folder) will report false to isDirectory(). */ this.file = file.getAbsoluteFile(); if (!this.file.exists()) { throw new FileNotFoundException("File \"" + this.file.getAbsolutePath() + "\" not found"); } path = this.file.toPath(); folder = this.file.isDirectory(); } public FilePermissions(Path path) throws FileNotFoundException { if (path == null) { throw new IllegalArgumentException("Path argument cannot be null"); } this.path = path; if (!Files.exists(this.path)) { throw new FileNotFoundException("File \"" + this.path + "\" not found"); } file = path.toFile(); folder = Files.isDirectory(this.path); } /** * Must always be called in a synchronized context */ private void checkPermissions(boolean checkRead, boolean checkWrite, boolean checkExecute) { if (read == null && checkRead) { try { path.getFileSystem().provider().checkAccess(path, AccessMode.READ); read = true; } catch (AccessDeniedException e) { if (path.toString().equals(e.getMessage())) { lastCause = "Insufficient permission to read permissions"; } else if ("Permissions does not allow requested access".equals(e.getMessage())) { lastCause = "Permissions don't allow read access"; } else { lastCause = e.getMessage(); } read = false; } catch (IOException e) { lastCause = e.getMessage(); read = false; } } if (write == null && checkWrite) { try { path.getFileSystem().provider().checkAccess(path, AccessMode.WRITE); write = true; } catch (AccessDeniedException e) { if (e.getMessage().endsWith("Permissions does not allow requested access")) { lastCause = "Permissions don't allow write access"; } else { lastCause = e.getMessage(); } write = false; } catch (FileSystemException e) { // A workaround for https://bugs.openjdk.java.net/browse/JDK-8034057 // and similar bugs, if we can't determine it the nio way, fall // back to actually testing it. LOGGER.trace( "Couldn't determine write permissions for \"{}\", falling back to write testing. The error was: {}", path.toString(), e.getMessage() ); if (folder) { write = testFolderWritable(); } else { write = testFileWritable(file); } } catch (IOException e) { lastCause = e.getMessage(); write = false; } } if (execute == null && checkExecute) { // To conform to the fact that on Linux root always implicit // execute permission regardless of explicit permissions if (Platform.isLinux() && FileUtil.isAdmin()) { execute = true; } else { try { path.getFileSystem().provider().checkAccess(path, AccessMode.EXECUTE); execute = true; } catch (AccessDeniedException e) { if (e.getMessage().endsWith("Permissions does not allow requested access")) { lastCause = "Permissions don't allow execute access"; } else { lastCause = e.getMessage(); } execute = false; } catch (IOException e) { lastCause = e.getMessage(); execute = false; } } } } /** * @return Whether the <code>File</code> object this <code>FilePermission</code> * object represents is a folder. */ public boolean isFolder() { return folder; } /** * @return Whether the file or folder is readable in the current context. */ public synchronized boolean isReadable() { checkPermissions(true, false, false); return read; } /** * @return Whether the file or folder is writable in the current context. */ public synchronized boolean isWritable() { checkPermissions(false, true, false); return write; } /** * @return Whether the file is executable in the current context, or if * folder listing is permitted in the current context if it's a folder. */ public synchronized boolean isExecutable() { checkPermissions(false, false, true); return execute; } /** * @return Whether the listing of the folder's content is permitted. * For this to be <code>true</code> {@link #isFolder()}, {@link #isReadable()} * and {@link #isExecutable()} must be true. */ public synchronized boolean isBrowsable() { checkPermissions(true, false, true); return folder && read && execute; } /** * @return The <code>File</code> object this <code>FilePermission</code> * object represents. */ public File getFile() { return file; } /** * @return The <code>Path</code> object this <code>FilePermission</code> * object represents. */ public Path getPath() { return path; } /** * Re-reads file or folder permissions in case they have changed. */ public synchronized void refresh() { read = null; write = null; execute = null; lastCause = null; } public synchronized String getLastCause() { return lastCause; } @Override public synchronized String toString() { checkPermissions(true, true, true); StringBuilder sb = new StringBuilder(); sb.append(folder ? "d" : "-"); if (read == null) { sb.append('?'); } else { sb.append(read ? "r" : "-"); } if (write == null) { sb.append('?'); } else { sb.append(write ? "w" : "-"); } if (execute == null) { sb.append('?'); } else { sb.append(execute ? "x" : "-"); } return sb.toString(); } /** * Must always be called in a synchronized context */ private boolean testFolderWritable() { if (!folder) { throw new IllegalStateException("Can only be called on a folder"); } boolean isWritable = false; File file = new File( this.file, String.format( "UMS_folder_write_test_%d_%d.tmp", System.currentTimeMillis(), Thread.currentThread().getId() ) ); try { if (file.createNewFile()) { if (testFileWritable(file)) { isWritable = true; } if (!file.delete()) { LOGGER.warn("Can't delete temporary test file: {}", file.getAbsolutePath()); } } } catch (IOException e) { lastCause = e.getMessage(); } return isWritable; } /** * Must always be called in a synchronized context */ private boolean testFileWritable(File file) { file = file.getAbsoluteFile(); if (file.isDirectory()) { throw new IllegalStateException("Can't be called on a folder"); } boolean isWritable = false; boolean fileAlreadyExists = file.isFile(); // i.e. exists and is a File if (fileAlreadyExists || !file.exists()) { try { // fileAlreadyExists: open for append: make sure the open // doesn't clobber the file new FileOutputStream(file, true).close(); isWritable = true; if (!fileAlreadyExists) { // a new file has been "touched"; try to remove it if (!file.delete()) { LOGGER.warn("Can't delete temporary test file: {}", file.getAbsolutePath()); } } } catch (IOException e) { lastCause = e.getMessage(); } } else { lastCause = file.getAbsolutePath() + " isn't a file"; } return isWritable; } }