/* * Copyright 2004 - 2008 Christian Sprajc. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder is 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. * * PowerFolder 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 PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id$ */ package de.dal33t.powerfolder.light; import java.io.File; import java.io.IOException; import java.io.InvalidClassException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.Serializable; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import de.dal33t.powerfolder.Controller; import de.dal33t.powerfolder.Member; import de.dal33t.powerfolder.clientserver.ServerClient; import de.dal33t.powerfolder.disk.Folder; import de.dal33t.powerfolder.disk.FolderRepository; import de.dal33t.powerfolder.util.DateUtil; import de.dal33t.powerfolder.util.ExternalizableUtil; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.StringUtils; import de.dal33t.powerfolder.util.Util; import de.dal33t.powerfolder.util.os.OSUtil; /** * File information of a local or remote file. NEVER USE A CONSTRUCTOR OF THIS * CLASS. YOU ARE DOING IT WRONG!. Use {@link FileInfoFactory} * * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a> * @version $Revision: 1.33 $ */ public class FileInfo implements Serializable, DiskItem, Cloneable { public static final String UNIX_SEPARATOR = "/"; private static final Logger log = Logger.getLogger(FileInfo.class.getName()); /** * #1531: If this system should ignore cases of files in * {@link #equals(Object)} and {@link #hashCode()} */ public static final boolean IGNORE_CASE = OSUtil.isWindowsSystem() || OSUtil.isMacOS(); public static final String PROPERTYNAME_FILE_NAME = "fileName"; public static final String PROPERTYNAME_SIZE = "size"; public static final String PROPERTYNAME_MODIFIED_BY = "modifiedBy"; public static final String PROPERTYNAME_LAST_MODIFIED_DATE = "lastModifiedDate"; public static final String PROPERTYNAME_VERSION = "version"; public static final String PROPERTYNAME_DELETED = "deleted"; public static final String PROPERTYNAME_FOLDER_INFO = "folderInfo"; private static final long serialVersionUID = 100L; /** * Unix-style separated path of the file relative to the folder base dir. * So like 'myFile.txt' or 'directory/myFile.txt' or 'directory/subdirectory/myFile.txt'. */ private String fileName; /** The size of the file */ private Long size; /** * modified info * * <p> * Actually 'final'. Only non-final because of serialization readObject() * MemberInfo.intern(); */ private MemberInfo modifiedBy; /** modified in folder on date */ private Date lastModifiedDate; /** Version number of this file */ private int version; /** the deleted flag */ private boolean deleted; /** * the folder. * <p> * Actually 'final'. Only non-final because of serialization readObject() * folderInfo.intern(); */ private FolderInfo folderInfo; /** * The cached hash info. */ private transient int hash; /** * Contains some cached string. */ private transient Reference<FileInfoStrings> cachedStrings; protected FileInfo() { // ONLY for backward compatibility to MP3FileInfo fileName = null; size = null; modifiedBy = null; lastModifiedDate = null; version = 0; deleted = false; folderInfo = null; // VERY IMPORANT. MUST BE DONE IN EVERY CONSTRUCTOR // this.hash = hashCode0(); } protected FileInfo(String relativeName, long size, MemberInfo modifiedBy, Date lastModifiedDate, int version, boolean deleted, FolderInfo folderInfo) { Reject.ifNull(folderInfo, "folder is null!"); Reject.ifNull(relativeName, "relativeName is null!"); if (relativeName.contains("../")) { throw new IllegalArgumentException( "relativeName must not contain ../: " + relativeName); } fileName = relativeName; this.size = size; this.modifiedBy = modifiedBy; this.lastModifiedDate = lastModifiedDate; this.version = version; this.deleted = deleted; this.folderInfo = folderInfo; validate(); // VERY IMPORANT. MUST BE DONE IN EVERY CONSTRUCTOR // NOT LONGER NEEDED this.hash = hashCode0(); } protected FileInfo(FolderInfo folder, String relativeName) { Reject.ifNull(folder, "folder is null!"); Reject.ifNull(relativeName, "relativeName is null!"); if (relativeName.contains("../")) { throw new IllegalArgumentException( "relativeName must not contain ../: " + relativeName); } fileName = relativeName; folderInfo = folder; size = null; modifiedBy = null; lastModifiedDate = null; version = 0; deleted = false; // VERY IMPORANT. MUST BE DONE IN EVERY CONSTRUCTOR // this.hash = hashCode0(); } /** * Syncs fileinfo with diskfile. If diskfile has other lastmodified date * that this. Assume that file has changed on disk and update its modified * info. * * @param folder * the folder to sync with * @param diskFile * the diskfile of this file, not gets it from controller ! * @return the new FileInfo if the file was synced or null if the file is in * sync */ public FileInfo syncFromDiskIfRequired(Folder folder, File diskFile) { Reject.ifNull(folder, "Folder is null"); Reject.ifFalse(folder.getInfo().equals(folderInfo), "Folder mismatch"); if (diskFile == null) { throw new NullPointerException("diskFile is null"); } String diskFileName = FileInfoFactory.decodeIllegalChars(diskFile .getName()); boolean nameMatch = fileName.endsWith(diskFileName); if (!nameMatch && IGNORE_CASE) { // Try harder if ignore case nameMatch = diskFileName.equalsIgnoreCase(getFilenameOnly()); } // Check if files match if (!nameMatch) { throw new IllegalArgumentException( "Diskfile does not match fileinfo name '" + getFilenameOnly() + "', details: " + toDetailString() + ", diskfile name '" + diskFile.getName() + "', path: " + diskFile); } // if (!diskFile.exists()) { // log.warning("File does not exsists on disk: " + toDetailString()); // } if (!inSyncWithDisk(diskFile)) { MemberInfo mySelf = folder.getController().getMySelf().getInfo(); if (diskFile.exists()) { return FileInfoFactory.modifiedFile(this, folder, diskFile, mySelf); } else { return FileInfoFactory.deletedFile(this, mySelf, new Date()); } } return null; } /** * @param diskFile * the file on disk. * @return true if the fileinfo is in sync with the file on disk. */ public boolean inSyncWithDisk(File diskFile) { return inSyncWithDisk0(diskFile, false); } /** * @param diskFile * the file on disk. * @param ignoreSizeAndModDate * ignore the reported size of the diskfile/dir. * @return true if the fileinfo is in sync with the file on disk. */ protected boolean inSyncWithDisk0(File diskFile, boolean ignoreSizeAndModDate) { Reject.ifNull(diskFile, "Diskfile is null"); boolean diskFileDeleted = !diskFile.exists(); boolean existanceSync = diskFileDeleted && deleted || !diskFileDeleted && !deleted; if (ignoreSizeAndModDate) { boolean dirFileSync = diskFileDeleted || (isDiretory() && diskFile.isDirectory()); return existanceSync && dirFileSync; } if (!existanceSync) { return false; } boolean lastModificationSync = DateUtil.equalsFileDateCrossPlattform( diskFile.lastModified(), lastModifiedDate.getTime()); if (!lastModificationSync) { return false; } boolean sizeSync = size == diskFile.length(); if (!sizeSync) { return false; } return true; // return existanceSync && lastModificationSync && sizeSync; } /** * @return the name , relative to the folder base. */ public String getRelativeName() { return fileName; } /** * @return The filename (including the path from the base of the folder) * converted to lowercase */ public String getLowerCaseFilenameOnly() { // if (Feature.CACHE_FILEINFO_STRINGS.isDisabled()) { // return getFilenameOnly0().toLowerCase(); // } FileInfoStrings strings = getStringsCache(); if (strings.getLowerCaseName() == null) { strings.setLowerCaseName(fileName.toLowerCase()); } return strings.getLowerCaseName(); } private FileInfoStrings getStringsCache() { FileInfoStrings stringsRef = cachedStrings != null ? cachedStrings .get() : null; if (stringsRef == null) { // Cache miss. create new entry stringsRef = new FileInfoStrings(); cachedStrings = new WeakReference<FileInfoStrings>(stringsRef); } return stringsRef; } /** * @return everything after the last point (.) in the fileName in upper case */ public String getExtension() { String tmpFileName = getFilenameOnly(); int index = tmpFileName.lastIndexOf('.'); if (index == -1) { return ""; } return tmpFileName.substring(index + 1, tmpFileName.length()) .toUpperCase(); } /** * Gets the filename only, without the directory structure * * @return the filename only of this file. */ public String getFilenameOnly() { // if (Feature.CACHE_FILEINFO_STRINGS.isDisabled()) { // return getFilenameOnly0(); // } FileInfoStrings strings = getStringsCache(); if (strings.getFileNameOnly() == null) { strings.setFileNameOnly(getFilenameOnly0()); } return strings.getFileNameOnly(); } private String getFilenameOnly0() { int index = fileName.lastIndexOf('/'); if (index > -1) { return fileName.substring(index + 1); } else { return fileName; } } /** * @return if this file was deleted. */ public boolean isDeleted() { return deleted; } /** * @param repo * @return if this file is expeced */ public boolean isExpected(FolderRepository repo) { if (deleted) { return false; } Folder folder = repo.getFolder(folderInfo); if (folder == null) { return false; } return !folder.isKnown(this); } /** * @param controller * @return if this file is currently downloading */ public boolean isDownloading(Controller controller) { return controller.getTransferManager().isDownloadingActive(this); } /** * @param controller * @return if this file is currently uploading */ public boolean isUploading(Controller controller) { return controller.getTransferManager().isUploading(this); } /** * @param controller * @return if the diskfile exists */ public boolean diskFileExists(Controller controller) { File diskFile = getDiskFile(controller.getFolderRepository()); return diskFile != null && diskFile.exists(); } /** * @return the size of the file. */ public long getSize() { return size; } /** * @return the modificator of this file. */ public MemberInfo getModifiedBy() { return modifiedBy; } /** * @return the modification date. */ public Date getModifiedDate() { return lastModifiedDate; } /** * @return the version of the file. */ public int getVersion() { return version; } public boolean isLookupInstance() { return size == null; } public boolean isDiretory() { return false; } public boolean isFile() { return true; } public boolean isBaseDirectory() { return StringUtils.isBlank(fileName); } /** * @return a lookup instance of the subdirectory this {@link FileInfo} is * located in. */ public DirectoryInfo getDirectory() { int i = fileName.lastIndexOf('/'); if (i < 0) { return FileInfoFactory.createBaseDirectoryInfo(folderInfo); } String dirName = fileName.substring(0, i); return FileInfoFactory.lookupDirectory(folderInfo, dirName); } /** * @param ofInfo * the other fileinfo. * @return if this file is newer than the other one. By file version, or * file modification date if version of both =0 */ public boolean isNewerThan(FileInfo ofInfo) { return isNewerThan(ofInfo, false); } protected boolean isNewerThan(FileInfo ofInfo, boolean ignoreLastModified) { if (ofInfo == null) { throw new NullPointerException("Other file is null"); } // if (Feature.DETECT_UPDATE_BY_VERSION.isDisabled()) { // // Directly detected by last modified // return DateUtil.isNewerFileDateCrossPlattform(lastModifiedDate, // ofInfo.lastModifiedDate); // } if (version == ofInfo.version) { if (ignoreLastModified) { return false; } return DateUtil.isNewerFileDateCrossPlattform(lastModifiedDate, ofInfo.lastModifiedDate); } return version > ofInfo.version; } /** * Also considers myself. * * @param repo * the folder repository * @return if there is a newer version available of this file */ public boolean isNewerAvailable(FolderRepository repo) { FileInfo newestFileInfo = getNewestVersion(repo); return newestFileInfo != null && newestFileInfo.isNewerThan(this); } /** * Also considers myself * * @param repo * @return the newest available version of this file */ public FileInfo getNewestVersion(FolderRepository repo) { if (repo == null) { throw new NullPointerException("FolderRepo is null"); } Folder folder = getFolder(repo); if (folder == null) { if (log.isLoggable(Level.FINER)) { log.finer("Unable to determine newest version. Folder not joined " + folderInfo); } return null; } FileInfo newestVersion = null; for (Member member : folder.getMembersAsCollection()) { FileInfo remoteFile = member.getFile(this); if (remoteFile == null) { continue; } if (!remoteFile.isValid()) { continue; } // Check if remote file in newer if (newestVersion == null || remoteFile.isNewerThan(newestVersion)) { // HACK(tm) if (!ServerClient.SERVER_HANDLE_MESSAGE_THREAD.get() && !folder.hasWritePermission(member)) { continue; } newestVersion = remoteFile; } } return newestVersion; } /** * @param repo * @return the newest available version of this file, excludes deleted * remote files */ public FileInfo getNewestNotDeletedVersion(FolderRepository repo) { if (repo == null) { throw new NullPointerException("FolderRepo is null"); } Folder folder = getFolder(repo); if (folder == null) { log.warning("Unable to determine newest version. Folder not joined " + folderInfo); return null; } FileInfo newestVersion = null; for (Member member : folder.getMembersAsCollection()) { if (member.isCompletelyConnected() || member.isMySelf()) { // Get remote file FileInfo remoteFile = member.getFile(this); if (remoteFile == null || remoteFile.deleted) { continue; } // Check if remote file is newer if (newestVersion == null || remoteFile.isNewerThan(newestVersion)) { // HACK(tm) if (!ServerClient.SERVER_HANDLE_MESSAGE_THREAD.get() && !folder.hasWritePermission(member)) { continue; } // log.finer("Newer version found at " + member); newestVersion = remoteFile; } } } return newestVersion; } /** * Resolves a file from local disk by folder repository, File MAY NOT Exist! * Returns null if folder was not found * * @param repo * @return the file. */ public File getDiskFile(FolderRepository repo) { Reject.ifNull(repo, "Repo is null"); Folder folder = getFolder(repo); if (folder == null) { return null; } return folder.getDiskFile(this); } /** * Resolves a FileInfo from local folder db by folder repository, File MAY * NOT Exist! Returns null if folder was not found * * @param repo * @return the FileInfo which is is in my own DB/knownfiles. */ public FileInfo getLocalFileInfo(FolderRepository repo) { Reject.ifNull(repo, "Repo is null"); Folder folder = getFolder(repo); if (folder == null) { return null; } return folder.getFile(this); } /** * @return the folderinfo this file belongs to. */ public FolderInfo getFolderInfo() { return folderInfo; } /** * @param repo * the folder repository. * @return the folder for this file. */ public Folder getFolder(FolderRepository repo) { if (repo == null) { throw new NullPointerException("Repository is null"); } return repo.getFolder(folderInfo); } /* * General */ /** * @param otherFile * @return true if the file name, version and date is equal. */ public boolean isVersionDateAndSizeIdentical(FileInfo otherFile) { if (otherFile == null) { return false; } if (version != otherFile.version) { // This is quick do it first return false; } if (!Util.equals(size, otherFile.size)) { return false; } if (!equals(otherFile)) { // not equals, return return false; } if (lastModifiedDate != null && otherFile.lastModifiedDate != null && !lastModifiedDate.equals(otherFile.lastModifiedDate)) { return false; } // All match! return true; } @Override public int hashCode() { if (hash == 0) { // Cache the hashcode hash = hashCode0(); } return hash; } private int hashCode0() { int hash = IGNORE_CASE ? fileName.toLowerCase().hashCode() : fileName .hashCode(); hash += folderInfo.hashCode(); return hash; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (other instanceof FileInfo) { FileInfo otherInfo = (FileInfo) other; boolean caseMatch = Util.equalsRelativeName(fileName, otherInfo.fileName); return caseMatch && Util.equals(folderInfo, otherInfo.folderInfo); } return false; } @Override public String toString() { return '[' + folderInfo.name + "]:" + (deleted ? "(del) /" : "/") + fileName; } /** * appends to buffer * * @param str * the stringbuilder to add the detail info to. */ private void toDetailString(StringBuilder str) { str.append(toString()); str.append(", size: "); str.append(size); str.append(" bytes, version: "); str.append(version); str.append(", modified: "); str.append(lastModifiedDate); str.append(" ("); if (lastModifiedDate != null) { str.append(lastModifiedDate.getTime()); } else { str.append("-n/a-"); } str.append(") by '"); if (modifiedBy == null) { str.append("-n/a-"); } else { str.append(modifiedBy.nick); } str.append('\''); } public String toDetailString() { StringBuilder str = new StringBuilder(); toDetailString(str); return str.toString(); } /** * @return true if this instance is valid. false if is broken,e.g. Negative * Time */ public boolean isValid() { try { validate(); return true; } catch (Exception e) { log.log(Level.WARNING, "Invalid: " + toDetailString() + ". " + e); return false; } } /** * Validates the state of the FileInfo. This should actually not be public - * checks should be made while constructing this class (by * constructor/deserialization). * * @throws IllegalArgumentException * if the state is corrupt */ private void validate() { Reject.ifNull(lastModifiedDate, "Modification date is null"); if (lastModifiedDate.getTime() < 0) { throw new IllegalStateException("Modification date is invalid: " + lastModifiedDate); } Reject.ifTrue(StringUtils.isEmpty(fileName), "Filename is empty"); char lastChar = fileName.charAt(fileName.length() - 1); if (lastChar == '/' || lastChar == '\\') { throw new IllegalStateException("Filename ends with slash: " + fileName); } Reject.ifNull(size, "Size is null"); Reject.ifFalse(size >= 0, "Negative file size"); Reject.ifNull(folderInfo, "FolderInfo is null"); } // Serialization optimization ********************************************* private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); // #2037: Removed internalization // fileName = fileName.intern(); // Oh! Default value. Better recalculate hashcode cache // if (hash == 0) { // hash = hashCode0(); // } folderInfo = folderInfo != null ? folderInfo.intern() : null; modifiedBy = modifiedBy != null ? modifiedBy.intern() : null; // #2159: Remove / in front and end of filename if (fileName.endsWith("/")) { fileName = fileName.substring(0, fileName.length() - 1); } if (fileName.startsWith("/")) { fileName = fileName.substring(1); } // validate(); } private static final long extVersionUID = 100L; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { long extUID = in.readLong(); if (extUID != extVersionUID) { throw new InvalidClassException(getClass().getName(), "Unable to read. extVersionUID(steam): " + extUID + ", expected: " + extVersionUID); } fileName = in.readUTF(); size = in.readLong(); if (in.readBoolean()) { modifiedBy = MemberInfo.readExt(in); modifiedBy = modifiedBy != null ? modifiedBy.intern() : null; } else { modifiedBy = null; } lastModifiedDate = ExternalizableUtil.readDate(in); version = in.readInt(); deleted = in.readBoolean(); folderInfo = ExternalizableUtil.readFolderInfo(in); folderInfo = folderInfo != null ? folderInfo.intern() : null; } public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(isFile() ? 0 : 1); out.writeLong(extVersionUID); out.writeUTF(fileName); out.writeLong(size); out.writeBoolean(modifiedBy != null); if (modifiedBy != null) { modifiedBy.writeExternal(out); } ExternalizableUtil.writeDate(out, lastModifiedDate); out.writeInt(version); out.writeBoolean(deleted); ExternalizableUtil.writeFolderInfo(out, folderInfo); } /** * Utility method for changing the fileName part of a relative file path. * Example renameRelativeFileName('directory/subdirectory/myFile.txt', 'newFile.txt') ==> * 'directory/subdirectory/newFile.txt' * * NOTE: This is static, so does not affect a FileInfo. * * @param relativeName * @param newFileName * @return */ public static String renameRelativeFileName(String relativeName, String newFileName) { if (newFileName.contains(UNIX_SEPARATOR)) { throw new IllegalArgumentException( "newFileName must not contain " + UNIX_SEPARATOR + ": " + relativeName); } if (relativeName.contains(UNIX_SEPARATOR)) { String directoryPart = relativeName.substring(0, relativeName.lastIndexOf(UNIX_SEPARATOR)); return directoryPart + UNIX_SEPARATOR + newFileName; } else { // No path - just use the relative filename. return newFileName; } } }