/*
* 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 java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import net.pms.util.FileUtil.InvalidFileSystemException;
import net.pms.util.FileUtil.UnixMountPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
public class FreedesktopTrash {
private static final Logger LOGGER = LoggerFactory.getLogger(FreedesktopTrash.class);
private static Path homeFolder = null;
private static Object homeFolderLock = new Object();
private static final String INFO = "info";
private static final String FILES = "files";
private static final SecureRandom random = new SecureRandom();
private static String generateRandomFileName(String fileName) {
if (fileName.contains("/") || fileName.contains("\\")) {
throw new IllegalArgumentException("Invalid file name");
}
String prefix;
String suffix;
if (fileName.contains(".")) {
int i = fileName.lastIndexOf('.');
prefix = fileName.substring(0, i);
suffix = fileName.substring(i);
} else {
prefix = fileName;
suffix = "";
}
long n = random.nextLong();
n = (n == Long.MIN_VALUE) ? 0 : Math.abs(n);
String newName = prefix + Long.toString(n) + suffix;
return newName;
}
private static Path getVerifiedPath(String location) {
if (location != null && !location.trim().isEmpty()) {
Path path = Paths.get(location);
if (Files.exists(path)) {
return path.toAbsolutePath();
}
}
return null;
}
private static Path getHomeFolder() {
synchronized (homeFolderLock) {
if (homeFolder == null) {
homeFolder = getVerifiedPath(System.getenv("XDG_DATA_HOME"));
if (homeFolder == null) {
homeFolder = getVerifiedPath(System.getenv("HOME"));
}
if (homeFolder == null) {
homeFolder = getVerifiedPath(System.getProperty("user.home"));
}
}
// Make a copy so we don't have to hold the lock
return Paths.get(homeFolder.toString());
}
}
private static boolean verifyTrashFolder(Path path, boolean create) {
FilePermissions permissions;
try {
permissions = new FilePermissions(path);
} catch (FileNotFoundException e) {
if (create) {
LOGGER.trace("Trash folder \"{}\" doesn't exist, attempting to create it", path);
try {
Files.createDirectories(path);
} catch (IOException e1) {
LOGGER.debug("Could not create user trash folder \"{}\": {}", path, e1.getMessage());
LOGGER.trace("", e1);
return false;
}
try {
permissions = new FilePermissions(path);
} catch (FileNotFoundException e1) {
LOGGER.error("Impossible situation in verifyTrashFolder()", e1);
return false;
}
} else {
LOGGER.trace("Trash folder \"{}\" doesn't exist", path);
LOGGER.trace("", e);
return false;
}
}
if (!(permissions.isBrowsable() && permissions.isWritable() && permissions.isFolder())) {
if (!permissions.isFolder()) {
LOGGER.debug("Trash folder \"{}\" is not a folder", path);
} else {
LOGGER.debug("Insufficient permissions for trash folder \"{}\": {}", path, permissions.toString());
}
return false;
}
return true;
}
private static Path getTrashFolder(Path path) throws InvalidFileSystemException, IOException {
UnixMountPoint pathMountPoint;
try {
pathMountPoint = FileUtil.getMountPoint(path);
} catch (InvalidFileSystemException e) {
throw new InvalidFileSystemException("Invalid file system for file: " + path.toAbsolutePath(), e);
}
Path homeFolder = getHomeFolder();
Path trashFolder;
if (homeFolder != null) {
UnixMountPoint homeMountPoint = null;
try {
homeMountPoint = FileUtil.getMountPoint(homeFolder);
} catch (InvalidFileSystemException e) {
LOGGER.trace(e.getMessage(), e);
// homeMountPoint == null is ok, fails on .equals()
}
if (pathMountPoint.equals(homeMountPoint)) {
// The file is on the same partition as the home folder,
// use home folder Trash
trashFolder = Paths.get(homeFolder.toString(), ".Trash");
if (!Files.exists(trashFolder)) {
// This is outside specification but follows convention
trashFolder = Paths.get(homeFolder.toString(), ".local/share/Trash");
}
if (verifyTrashFolder(trashFolder, true)) {
return trashFolder;
} else {
return null;
}
}
}
// The file is on a different partition than the home folder
// or no home folder was found, look for $topdir/.Trash.
trashFolder = Paths.get(pathMountPoint.folder, ".Trash");
if (Files.exists(trashFolder, LinkOption.NOFOLLOW_LINKS)) {
if (!Files.isSymbolicLink(trashFolder)) {
try {
if (FileUtil.isUnixStickyBit(trashFolder)) {
if (verifyTrashFolder(trashFolder, false)) {
try {
trashFolder = Paths.get(trashFolder.toString(), String.valueOf(FileUtil.getUnixUID()));
if (verifyTrashFolder(trashFolder, true)) {
return trashFolder;
} else {
LOGGER.trace("Could not read or create trash folder \"{}\", trying next option", trashFolder);
}
} catch (IOException e) {
LOGGER.trace("Could not determine user id while resolving trash folder, trying next option", e);
}
} else {
LOGGER.trace("Insufficient permissions for trash folder \"{}\", trying next option", trashFolder);
}
} else {
LOGGER.trace("Trash folder \"{}\" doesn't have sticky bit set, trying next option", trashFolder);
}
} catch (IOException e) {
LOGGER.trace("Could not determine sticky bit for trash folder \"" + trashFolder + "\", trying next option", e);
}
} else {
LOGGER.trace("Trash folder \"{}\" is a symbolic link, trying next option");
}
} else {
LOGGER.trace("Trash folder \"{}\" doesn't exist, trying next option", trashFolder);
}
// $topdir/.Trash not found, looking for $topdir/.Trash-$uid
try {
trashFolder = Paths.get(pathMountPoint.folder, ".Trash-" + FileUtil.getUnixUID());
} catch (IOException e) {
throw new IOException("Could not determine user id while resolving trash folder: " + e.getMessage(), e);
}
if (verifyTrashFolder(trashFolder, true)) {
return trashFolder;
} else {
LOGGER.debug("Unable to read or create trash folder \"{}\"", trashFolder);
return null;
}
}
private static boolean verifyTrashStructure(Path trashPath) {
String trashLocation = trashPath.toAbsolutePath().toString();
return verifyTrashFolder(Paths.get(trashLocation, INFO), true) && verifyTrashFolder(Paths.get(trashLocation, FILES), true);
}
public static void moveToTrash(Path path) throws InvalidFileSystemException, IOException {
if (path == null) {
throw new NullPointerException("path cannot be null");
}
final int LIMIT = 10;
path = path.toAbsolutePath();
FilePermissions pathPermissions = new FilePermissions(path);
if (!pathPermissions.isReadable() || !pathPermissions.isWritable()) {
throw new IOException("Insufficient permission to delete \"" + path.toString() + "\" - move to trash bin failed");
}
Path trashFolder = getTrashFolder(path);
Path infoFolder = trashFolder.resolve(INFO);
Path filesFolder = trashFolder.resolve(FILES);
if (!verifyTrashFolder(infoFolder, true) || !verifyTrashFolder(filesFolder, true)) {
throw new IOException(
"Could not move \"" + path.toString() + "\" to trash bin because " +
"of insufficient permissions for trash bin \"" + trashFolder.toString() + "\""
);
}
// Create the trash info
List<String> infoContent = new ArrayList<>();
infoContent.add("[Trash Info]");
infoContent.add("Path=" + URLEncoder.encode(path.toString(), Charset.defaultCharset().name()));
infoContent.add("DeletionDate=" + new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss").format(new Date()));
// Create the trash info file
Path infoFile = path.getFileName();
String fileName = infoFile != null ? infoFile.toString() : "";
int count = 0;
boolean created = false;
while (!created && count < LIMIT) {
infoFile = infoFolder.resolve(fileName + ".trashinfo");
try {
Files.write(infoFile, infoContent, Charset.defaultCharset(), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
created = true;
} catch (IOException e) {
fileName = generateRandomFileName(fileName);
count++;
}
}
if (!created) {
throw new IOException("Could not find a target filename for \"" + path.toString() + "\" in trash bin");
}
Path targetPath = filesFolder.resolve(fileName);
if (Files.exists(targetPath)) {
throw new IOException(
"Could not move \"" + path.toString() + "\" to trash bin since the trash bin \"" +
trashFolder.toString() + "\" is corrupted"
);
}
// Move the actual files
Files.move(path, targetPath, StandardCopyOption.ATOMIC_MOVE);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("{}{}\" moved to \"{}\"", Files.isDirectory(path) ? "Folder \"" : "File \"", path, targetPath);
}
}
public static void moveToTrash(File file) throws InvalidFileSystemException, IOException {
moveToTrash(file.toPath());
}
/**
* Tries to determine if FreeDesktop.org Trash specification is
* applicable for the given {@link Path}. Support can vary between
* partitions on the same computer. Please note that this check will
* look for or try to create the necessary folder structure, so this can
* be an expensive operation.
*
* The check could be used to evaluate a systems general ability, but a
* better strategy is to attempt {@link #moveToTrash(Path)} and handle
* the {@link Exception} if it fails.
*
* @param path the path for which to evaluate trash bin support
* @return The evaluation result
*/
public static boolean hasTrash(Path path) {
try {
return verifyTrashStructure(getTrashFolder(path));
} catch (IOException | InvalidFileSystemException e) {
LOGGER.warn("Error while trying to evaluate trash bin support: " + e.getMessage());
LOGGER.trace("", e);
return false;
}
}
/**
* Tries to determine if FreeDesktop.org Trash specification is
* applicable for the given {@link File}. Support can vary between
* partitions on the same computer. Please note that this check will
* look for or try to create the necessary folder structure, so this can
* be an expensive operation.
*
* The check could be used to evaluate a systems general ability, but a
* better strategy is to attempt {@link #moveToTrash(File)} and handle
* the {@link Exception} if it fails.
*
* @param path the path for which to evaluate trash bin support
* @return The evaluation result
*/
public static boolean hasTrash(File file) {
return hasTrash(file.toPath());
}
/**
* Tries to determine if FreeDesktop.org Trash specification is
* applicable for the system root. Support can vary between partitions
* on the same computer. Please note that this check will look for or
* try to create the necessary folder structure, so this can be an
* expensive operation.
*
* The check could be used to evaluate a systems general ability, but a
* better strategy is to attempt {@link #moveToTrash(File)} and handle
* the {@link Exception} if it fails.
*
* @param path the path for which to evaluate trash bin support
* @return The evaluation result
*/
@SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
public static boolean hasTrash() {
return hasTrash(Paths.get("/"));
}
}