diff --git a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java index 3561f3297bfc3aaa5f39b9eb3f01952af8bd3d3a..b2af7f8fa46f3e85cf57ab31d2ebe2869971829d 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java +++ b/bindings/java/src/org/sleuthkit/datamodel/AbstractFile.java @@ -43,7 +43,7 @@ * to the case. */ public abstract class AbstractFile extends AbstractContent { - + private static final Logger logger = Logger.getLogger(AbstractFile.class.getName()); protected final TskData.TSK_DB_FILES_TYPE_ENUM fileType; protected final TSK_FS_NAME_TYPE_ENUM dirType; protected final TSK_FS_META_TYPE_ENUM metaType; @@ -61,6 +61,7 @@ public abstract class AbstractFile extends AbstractContent { private String localPath; ///< local path as stored in db tsk_files_path, is relative to the db, private String localAbsPath; ///< absolute path representation of the local path private volatile RandomAccessFile localFileHandle; + private volatile boolean errorLoadingFromFileRepo = false; private volatile java.io.File localFile; private TskData.EncodingType encodingType; //range support @@ -781,6 +782,13 @@ public boolean isDirNameFlagSet(TSK_FS_NAME_FLAG_ENUM flag) { public String getDirFlagAsString() { return dirFlag.toString(); } + + /** + * @return The directory flag value. + */ + public TSK_FS_NAME_FLAG_ENUM getDirFlag() { + return dirFlag; + } /** * @return a string representation of the meta flags @@ -795,6 +803,13 @@ public String getMetaFlagsAsString() { return str; } + /** + * @return The meta flags as stored in the database. + */ + public short getMetaFlagsAsInt() { + return TSK_FS_META_FLAG_ENUM.toInt(metaFlags); + } + /** * @param metaFlag the TSK_FS_META_FLAG_ENUM to check * @@ -808,7 +823,7 @@ public boolean isMetaFlagSet(TSK_FS_META_FLAG_ENUM metaFlag) { public final int read(byte[] buf, long offset, long len) throws TskCoreException { //template method //if localPath is set, use local, otherwise, use readCustom() supplied by derived class - if (localPathSet) { + if ((location == TskData.FileLocation.REPOSITORY) || localPathSet) { return readLocal(buf, offset, len); } else { return readInt(buf, offset, len); @@ -843,7 +858,7 @@ protected int readInt(byte[] buf, long offset, long len) throws TskCoreException * @throws TskCoreException exception thrown when file could not be read */ protected final int readLocal(byte[] buf, long offset, long len) throws TskCoreException { - if (!localPathSet) { + if ((location == TskData.FileLocation.LOCAL) && !localPathSet) { throw new TskCoreException( BUNDLE.getString("AbstractFile.readLocal.exception.msg1.text")); } @@ -1054,12 +1069,18 @@ private synchronized void loadLocalFile() throws TskCoreException { localFile = new java.io.File(localAbsPath); } } else { + if (errorLoadingFromFileRepo == true) { + // Don't try to download it again + throw new TskCoreException("Previously failed to load file with object ID " + getId() + " from file repository."); + } + // Copy the file from the server - FileRepository fileRepo = FileRepository.getInstance(); - if (fileRepo != null) { - localFile = fileRepo.downloadFromFileRepository(this); - } else { - throw new TskCoreException("Error loading remote file with object ID " + getId() + ": file repository is not enabled"); + try { + localFile = FileRepository.downloadFromFileRepository(this); + } catch (TskCoreException ex) { + // If we've failed to download from the file repository, don't try again for this session. + errorLoadingFromFileRepo = true; + throw ex; } } } diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties index c3da2ea73394283627e42d91e29f5c069ecb2841..79874e99236f6dcdc95a0702f74ef4cd00be414d 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties @@ -352,4 +352,7 @@ IntersectionFilter.displayName.text=Intersection tagsFilter.displayName.text=Must be tagged TextFilter.displayName.text=Must include text: TypeFilter.displayName.text=Limit event types to -FileTypesFilter.displayName.text=Limit file types to \ No newline at end of file +FileTypesFilter.displayName.text=Limit file types to +FileRepository.downloadError.title.text=Error downloading from file repository +FileRepository.downloadError.msg.text=Failed to download file with object ID {0} and SHA-256 hash {1} +FileRepository.notEnabled.msg.text=File repository is not enabled diff --git a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED index c3da2ea73394283627e42d91e29f5c069ecb2841..79874e99236f6dcdc95a0702f74ef4cd00be414d 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED +++ b/bindings/java/src/org/sleuthkit/datamodel/Bundle.properties-MERGED @@ -352,4 +352,7 @@ IntersectionFilter.displayName.text=Intersection tagsFilter.displayName.text=Must be tagged TextFilter.displayName.text=Must include text: TypeFilter.displayName.text=Limit event types to -FileTypesFilter.displayName.text=Limit file types to \ No newline at end of file +FileTypesFilter.displayName.text=Limit file types to +FileRepository.downloadError.title.text=Error downloading from file repository +FileRepository.downloadError.msg.text=Failed to download file with object ID {0} and SHA-256 hash {1} +FileRepository.notEnabled.msg.text=File repository is not enabled diff --git a/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java b/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java index 346de5d11bfc94936835f7bf0614b4c3bbe871ab..0e676e4ee6313e92d0ea0cfa82c0834281453d5d 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java +++ b/bindings/java/src/org/sleuthkit/datamodel/DerivedFile.java @@ -71,6 +71,8 @@ public class DerivedFile extends AbstractFile { * @param mtime The modified time of the file. * @param md5Hash The MD5 hash of the file, null if not yet * calculated. + * @param sha256Hash The SHA-256 hash of the file, null if not yet + * calculated. * @param knownState The known state of the file from a hash * database lookup, null if not yet looked up. * @param parentPath The path of the parent of the file. diff --git a/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java b/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java index 141ccce7539c973b10213f2efb1ba5b57e334041..6c4b456b4acc1f195adbeda028df9c0efa0a5b7a 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java +++ b/bindings/java/src/org/sleuthkit/datamodel/FileRepository.java @@ -21,17 +21,24 @@ import java.io.File; import java.io.IOException; import java.nio.file.Paths; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Class to represent a file repository. */ public class FileRepository { + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("org.sleuthkit.datamodel.Bundle"); + private static final Logger logger = Logger.getLogger(FileRepository.class.getName()); + private static FileRepositoryErrorHandler errorHandler; private final static String FILE_PATH = "v1/files/"; private final FileRepositorySettings settings; - private final String fileDownloadFolder; + private final File fileDownloadFolder; private static FileRepository instance; @@ -41,7 +48,7 @@ public class FileRepository { * @param settings The file repository settings * @param fileDownloadPath The temporary folder to download files to from the repository */ - private FileRepository(FileRepositorySettings settings, String fileDownloadPath) { + private FileRepository(FileRepositorySettings settings, File fileDownloadPath) { this.settings = settings; this.fileDownloadFolder = fileDownloadPath; } @@ -52,7 +59,12 @@ private FileRepository(FileRepositorySettings settings, String fileDownloadPath) * @param settings The file repository settings * @param fileDownloadPath The temporary folder to download files to from the repository */ - public static synchronized void initialize(FileRepositorySettings settings, String fileDownloadPath) { + public static synchronized void initialize(FileRepositorySettings settings, File fileDownloadPath) { + // If the download path is changing, delete any files in the old one + if ((instance != null) && (instance.fileDownloadFolder != null) + && ( ! instance.fileDownloadFolder.equals(fileDownloadPath))) { + deleteDownloadFolder(instance.fileDownloadFolder); + } instance = new FileRepository(settings, fileDownloadPath); } @@ -60,20 +72,74 @@ public static synchronized void initialize(FileRepositorySettings settings, Stri * De-initializes the file repository. */ public static synchronized void deinitialize() { + if (instance != null) { + // Delete the temp folder + deleteDownloadFolder(instance.fileDownloadFolder); + } + instance = null; } /** - * Gets the instance of the file repository. + * Check if the file repository is enabled. * - * @return The instance of the file repository. Will be null if not initialized. - */ - public static synchronized FileRepository getInstance() { - return instance; + * @return true if enabled, false otherwise. + */ + public static boolean isEnabled() { + return instance != null; + } + + /** + * Set the error handling callback. + * + * @param handler The error handler. + */ + public static synchronized void setErrorHandler(FileRepositoryErrorHandler handler) { + errorHandler = handler; + } + + /** + * Report an error to the user. + * The idea is to use this for cases where it's a user error that may be able + * to be corrected through changing the repository settings. + * + * @param errorTitle The title for the error display. + * @param errorStr The error message. + */ + private static synchronized void reportError(String errorTitle, String errorStr) { + if (errorHandler != null) { + errorHandler.displayErrorToUser(errorTitle, errorStr); + } + } + + /** + * Delete the folder of downloaded files. + */ + private static synchronized void deleteDownloadFolder(File dirPath) { + if (dirPath.isDirectory() == false || dirPath.exists() == false) { + return; + } + + File[] files = dirPath.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDownloadFolder(file); + } else { + if (file.delete() == false) { + logger.log(Level.WARNING, "Failed to delete file {0}", file.getPath()); //NON-NLS + } + } + } + } + if (dirPath.delete() == false) { + logger.log(Level.WARNING, "Failed to delete the empty directory at {0}", dirPath.getPath()); //NON-NLS + } } /** * Download a file from the file repository. + * The caller must ensure that this is not called on the same file multiple times concurrently. * * @param abstractFile The file being downloaded. * @@ -81,8 +147,15 @@ public static synchronized FileRepository getInstance() { * * @throws TskCoreException */ - public File downloadFromFileRepository(AbstractFile abstractFile) throws TskCoreException { + public static synchronized File downloadFromFileRepository(AbstractFile abstractFile) throws TskCoreException { + if (instance == null) { + String title = BUNDLE.getString("FileRepository.downloadError.title.text"); + String msg = BUNDLE.getString("FileRepository.notEnabled.msg.text"); + reportError(title, msg); + throw new TskCoreException("File repository is not enabled"); + } + if (! abstractFile.getFileLocation().equals(TskData.FileLocation.REPOSITORY)) { throw new TskCoreException("File with object ID " + abstractFile.getId() + " is not stored in the file repository"); } @@ -92,17 +165,19 @@ public File downloadFromFileRepository(AbstractFile abstractFile) throws TskCore } // Download the file if it's not already there. - String targetPath = Paths.get(fileDownloadFolder, abstractFile.getSha256Hash()).toString(); + String targetPath = Paths.get(instance.fileDownloadFolder.getAbsolutePath(), abstractFile.getSha256Hash()).toString(); if ( ! new File(targetPath).exists()) { - downloadFile(abstractFile, targetPath); + instance.downloadFile(abstractFile, targetPath); } // Check that we got the file. File tempFile = new File(targetPath); if (tempFile.exists()) { - System.out.println("Got file " + targetPath); // TODO REMOVE return tempFile; } else { + String title = BUNDLE.getString("FileRepository.downloadError.title.text"); + String msg = MessageFormat.format(BUNDLE.getString("FileRepository.downloadError.msg.text"), abstractFile.getId(), abstractFile.getSha256Hash()); + reportError(title, msg); throw new TskCoreException("Failed to download file with object ID " + abstractFile.getId() + " and SHA-256 hash " + abstractFile.getSha256Hash() + " from file repository"); } @@ -134,8 +209,13 @@ private void downloadFile(AbstractFile abstractFile, String targetPath) throws T try { Process process = processBuilder.start(); process.waitFor(); - } catch (IOException | InterruptedException ex) { + } catch (IOException ex) { + String title = BUNDLE.getString("FileRepository.downloadError.title.text"); + String msg = MessageFormat.format(BUNDLE.getString("FileRepository.downloadError.msg.text"), abstractFile.getId(), abstractFile.getSha256Hash()); + reportError(title, msg); throw new TskCoreException("Error downloading file with SHA-256 hash " + abstractFile.getSha256Hash() + " from file repository", ex); + } catch (InterruptedException ex) { + throw new TskCoreException("Interrupted while downloading file with SHA-256 hash " + abstractFile.getSha256Hash() + " from file repository", ex); } } @@ -143,16 +223,22 @@ private void downloadFile(AbstractFile abstractFile, String targetPath) throws T * Upload a given file to the file repository. * * @param filePath The path on disk to the file being uploaded. + * + * @throws TskCoreException */ - public void uploadToFileRepository(String filePath) throws TskCoreException { + public static synchronized void uploadToFileRepository(String filePath) throws TskCoreException { + if (instance == null) { + throw new TskCoreException("File repository is not enabled"); + } + File file = new File(filePath); if (! file.exists()) { throw new TskCoreException("Error uploading file " + filePath + " to file repository - file does not exist"); } // Upload the file. - uploadFile(file); + instance.uploadFile(file); } /** @@ -213,4 +299,17 @@ String getPort() { return port; } } + + /** + * Callback class to use for error reporting. + */ + public interface FileRepositoryErrorHandler { + /** + * Handles displaying an error message to the user (if appropriate). + * + * @param title The title for the error display. + * @param error The more detailed error message to display. + */ + void displayErrorToUser(String title, String error); + } } diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java index 2ef187a4ab6b68c176156096c4a5bbb0ac066efb..3c171a0648b04bab432d5a4845fd07b35548a82a 100644 --- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java +++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitCase.java @@ -6192,6 +6192,122 @@ public FileSystem addFileSystem(long parentObjId, long imgOffset, TskData.TSK_FS releaseSingleUserCaseWriteLock(); } } + + /** + * Add a file system file to the database by supplying all fields to copy into the + * tsk_files entry. This should generally only be used when the location + * field is not set to LOCAL since it does not create any additional database + * entries for reading the file data (such as rows in tsk_files_path for local files, + * rows in tsk_file_layout for layout files, etc). + * + * @param fsObjId The fs object ID or null + * @param fileName The name of the file. + * @param fileType The type of file + * @param metaAddr The meta address of the file. + * @param metaSeq The meta address sequence of the file. + * @param attrType The attributed type of the file. + * @param attrId The attribute id + * @param dirFlag The allocated status from the name structure + * @param metaFlags The meta flags. + * @param size The size of the file in bytes. + * @param ctime The changed time of the file. + * @param crtime The creation time of the file. + * @param atime The accessed time of the file + * @param mtime The modified time of the file. + * @param md5 The MD5 hash of the file (may be null). + * @param sha256 The SHA-256 hash of the file (may be null). + * @param known The FileKnown value of the file. + * @param mimeType The MIME type of the file (may be null). + * @param isFile True, unless the file is a directory. + * @param location The location the file is stored. + * @param parent The parent of the file (e.g., a virtual directory) + * @param transaction The current transaction + * + * @return Newly created file + * + * @throws TskCoreException + */ + public AbstractFile addFileSystemFile(long fsObjId, + String fileName, + TskData.TSK_DB_FILES_TYPE_ENUM fileType, + long metaAddr, long metaSeq, + TSK_FS_ATTR_TYPE_ENUM attrType, int attrId, + TSK_FS_NAME_FLAG_ENUM dirFlag, short metaFlags, long size, + long ctime, long crtime, long atime, long mtime, + String md5, String sha256, FileKnown known, String mimeType, + boolean isFile, TskData.FileLocation location, Content parent, + CaseDbTransaction transaction) throws TskCoreException { + + TimelineManager timelineManager = getTimelineManager(); + Statement queryStatement = null; + try { + CaseDbConnection connection = transaction.getConnection(); + + // Insert a row for the local/logical file into the tsk_objects table. + // INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?) + long objectId = addObject(parent.getId(), TskData.ObjectType.ABSTRACTFILE.getObjectType(), connection); + + String parentPath; + long dataSourceObjId; + + if (parent instanceof AbstractFile) { + AbstractFile parentFile = (AbstractFile) parent; + if (isRootDirectory(parentFile, transaction)) { + parentPath = "/"; + } else { + parentPath = parentFile.getParentPath() + parent.getName() + "/"; //NON-NLS + } + dataSourceObjId = parentFile.getDataSourceObjectId(); + } else { + parentPath = "/"; + dataSourceObjId = getDataSourceObjectId(connection, parent.getId()); + } + + PreparedStatement statement = connection.getPreparedStatement(PREPARED_STATEMENT.INSERT_FILE_SYSTEM_FILE); + statement.clearParameters(); + statement.setLong(1, objectId); // obj_is + statement.setLong(2, fsObjId); // fs_obj_id + statement.setLong(3, dataSourceObjId); // data_source_obj_id + statement.setShort(4, (short) attrType.getValue()); // attr_type + statement.setInt(5, attrId); // attr_id + statement.setString(6, fileName); // name + statement.setLong(7, metaAddr); // meta_addr + statement.setInt(8, (int)metaSeq); // meta_addr + statement.setShort(9, fileType.getFileType()); //type + statement.setShort(10, (short) 1); // has_path + TSK_FS_NAME_TYPE_ENUM dirType = isFile ? TSK_FS_NAME_TYPE_ENUM.REG : TSK_FS_NAME_TYPE_ENUM.DIR; + statement.setShort(11, dirType.getValue()); // dir_type + TSK_FS_META_TYPE_ENUM metaType = isFile ? TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG : TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR; + statement.setShort(12, metaType.getValue()); // meta_type + statement.setShort(13, dirFlag.getValue()); // dir_flags + statement.setShort(14, metaFlags); // meta_flags + statement.setLong(15, size < 0 ? 0 : size); + statement.setLong(16, ctime); + statement.setLong(17, crtime); + statement.setLong(18, atime); + statement.setLong(19, mtime); + statement.setString(20, md5); // MD5 + statement.setString(21, sha256); // SHA-256 + statement.setByte(22, known.getFileKnownValue()); // Known + statement.setString(23, mimeType); // MIME type + statement.setString(24, parentPath); + statement.setLong(25, location.getValue()); + final String extension = extractExtension(fileName); + statement.setString(26, extension); + + connection.executeUpdate(statement); + + DerivedFile derivedFile = new DerivedFile(this, objectId, dataSourceObjId, fileName, dirType, metaType, dirFlag, metaFlags, + size, ctime, crtime, atime, mtime, md5, sha256, known, parentPath, mimeType, TskData.FileLocation.LOCAL, parent.getId(), null, null, extension); + timelineManager.addEventsForNewFile(derivedFile, connection); + + return getAbstractFileById(objectId, connection); + } catch (SQLException ex) { + throw new TskCoreException("Failed to add file system file", ex); + } finally { + closeStatement(queryStatement); + } + } /** * Add a file system file. @@ -6273,16 +6389,19 @@ public FsContent addFileSystemFile(long dataSourceObjId, long fsObjId, statement.setLong(17, crtime); statement.setLong(18, atime); statement.setLong(19, mtime); - statement.setString(20, parentPath); - statement.setLong(21, TskData.FileLocation.LOCAL.getValue()); + statement.setNull(20, java.sql.Types.VARCHAR); // MD5 + statement.setNull(21, java.sql.Types.VARCHAR); // SHA-256 + statement.setByte(22, FileKnown.UNKNOWN.getFileKnownValue()); // Known + statement.setNull(23, java.sql.Types.VARCHAR); // MIME type + statement.setString(24, parentPath); + statement.setLong(25, TskData.FileLocation.LOCAL.getValue()); final String extension = extractExtension(fileName); - statement.setString(22, extension); + statement.setString(26, extension); connection.executeUpdate(statement); DerivedFile derivedFile = new DerivedFile(this, objectId, dataSourceObjId, fileName, dirType, metaType, dirFlag, metaFlags, size, ctime, crtime, atime, mtime, null, null, null, parentPath, null, TskData.FileLocation.LOCAL, parent.getId(), null, null, extension); - timelineManager.addEventsForNewFile(derivedFile, connection); transaction.commit(); @@ -11252,8 +11371,8 @@ private enum PREPARED_STATEMENT { INSERT_OBJECT("INSERT INTO tsk_objects (par_obj_id, type) VALUES (?, ?)"), //NON-NLS INSERT_FILE("INSERT INTO tsk_files (obj_id, fs_obj_id, name, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, md5, sha256, known, mime_type, parent_path, location, data_source_obj_id,extension) " //NON-NLS + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), //NON-NLS - INSERT_FILE_SYSTEM_FILE("INSERT INTO tsk_files(obj_id, fs_obj_id, data_source_obj_id, attr_type, attr_id, name, meta_addr, meta_seq, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, parent_path, location, extension)" - + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), // NON-NLS + INSERT_FILE_SYSTEM_FILE("INSERT INTO tsk_files(obj_id, fs_obj_id, data_source_obj_id, attr_type, attr_id, name, meta_addr, meta_seq, type, has_path, dir_type, meta_type, dir_flags, meta_flags, size, ctime, crtime, atime, mtime, md5, sha256, known, mime_type, parent_path, location, extension)" + + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), // NON-NLS UPDATE_DERIVED_FILE("UPDATE tsk_files SET type = ?, dir_type = ?, meta_type = ?, dir_flags = ?, meta_flags = ?, size= ?, ctime= ?, crtime= ?, atime= ?, mtime= ?, mime_type = ? " + "WHERE obj_id = ?"), //NON-NLS INSERT_LAYOUT_FILE("INSERT INTO tsk_file_layout (obj_id, byte_start, byte_len, sequence) " //NON-NLS