/// <summary>
        /// Creates a new randomly-named backup directory in the given directory. Creates the given directory too, if
        /// required.
        /// </summary>
        /// <param name="targetPath">The path of the directory in which to create the backup directory.</param>
        /// <returns>The name of the new backup directory.</returns>
        /// <exception cref="BackupDirectoryCreateException">If the new directory could not be created, due to I/O
        /// errors, permission errors, etc.</exception>
        public static string CreateBackupDirectory(string targetPath)
        {
            var           retries = BACKUP_DIRECTORY_CREATION_RETRIES;
            List <string> attemptedDirectoryNames = new();

            while (true)
            {
                var name = Utility.RandomAlphaNumericString(BACKUP_DIRECTORY_NAME_LENGTH);
                attemptedDirectoryNames.Add(name);

                var path = BackupPath(targetPath, name);

                FilesystemException?exception = null;
                // Non-atomicity :|
                if (!Directory.Exists(path) && !File.Exists(path))
                {
                    try {
                        FilesystemException.ConvertSystemException(() => Directory.CreateDirectory(path), () => path);
                        return(name);
                    }
                    catch (FilesystemException e) {
                        exception = e;
                    }
                }

                if (retries <= 0)
                {
                    throw new BackupDirectoryCreateException(targetPath, attemptedDirectoryNames, exception);
                }
                retries--;
            }
        }
Beispiel #2
0
        /// <summary>
        /// Parses and validates the application's command line arguments.
        /// </summary>
        /// <remarks>
        /// Note that the filesystem paths in the returned <see cref="AppConfig"/> are not guaranteed to be valid, as
        /// this is unfortunately not really possible to check without actually doing the desired I/O operation. <br/>
        /// However, some invalid paths are detected by this method.
        /// </remarks>
        /// <param name="args">The command line arguments.</param>
        /// <returns>The <see cref="AppConfig"/> parsed from the arguments.</returns>
        /// <exception cref="InvalidCmdArgsError">If the command line arguments are invalid.</exception>
        /// <exception cref="ApplicationRuntimeError">If the config paths can't be resolved.</exception>
        private AppConfig ParseCmdArgs(string[] args)
        {
            if (args.Length < 2)
            {
                throw new InvalidCmdArgsError();
            }

            var sourcePath   = args[0];
            var targetPath   = args[1];
            var excludePaths = args.Skip(2);

            try {
                sourcePath = FilesystemException.ConvertSystemException(() => Path.GetFullPath(sourcePath),
                                                                        () => sourcePath);
            }
            catch (FilesystemException e) {
                throw new ApplicationRuntimeError($"Failed to resolve source directory: {e.Reason}");
            }

            if (File.Exists(sourcePath))
            {
                throw new ApplicationRuntimeError("Source directory is not a directory");
            }
            if (!Directory.Exists(sourcePath))
            {
                throw new ApplicationRuntimeError("Source directory not found");
            }

            try {
                targetPath = FilesystemException.ConvertSystemException(() => Path.GetFullPath(targetPath),
                                                                        () => targetPath);
            }
            catch (FilesystemException e) {
                throw new ApplicationRuntimeError($"Failed to resolve target directory: {e.Reason}");
            }

            if (File.Exists(targetPath))
            {
                throw new ApplicationRuntimeError("Target directory exists and is not a directory.");
            }

            List <string> parsedExcludePaths = new();

            foreach (var path in excludePaths)
            {
                string fullPath;
                try {
                    fullPath = FilesystemException.ConvertSystemException(
                        () => Path.GetFullPath(path, sourcePath), () => path);
                }
                catch (FilesystemException e) {
                    Logger.Warning($"Failed to resolve exclude path \"{path}\" ({e.Reason}); discarding.");
                    continue;
                }
                parsedExcludePaths.Add(fullPath);
            }

            return(new(sourcePath, targetPath, parsedExcludePaths));
        }
Beispiel #3
0
 /// <summary>
 /// Opens a backup index file for reading.
 /// </summary>
 /// <param name="filePath">The path to the index file.</param>
 /// <returns>A new <see cref="StreamReader"/> associated with the file.</returns>
 /// <exception cref="BackupIndexFileIOException">If the file could not be opened.</exception>
 private static StreamReader OpenFile(string filePath)
 {
     try {
         return(FilesystemException.ConvertSystemException(
                    () => new StreamReader(filePath, new UTF8Encoding(false, true)), () => filePath));
     }
     catch (FilesystemException e) {
         throw new BackupIndexFileIOException(filePath, e);
     }
 }
Beispiel #4
0
 /// <summary>
 /// Creates a handler that logs to a file at the given path. <br/>
 /// The file is created new or overwritten if it exists.
 /// </summary>
 /// <param name="path">The file to log messages to.</param>
 /// <exception cref="LoggingException">If the file could not be opened.</exception>
 public FileLogHandler(string path)
 {
     try {
         Stream = FilesystemException.ConvertSystemException(() => File.CreateText(path), () => path);
     }
     catch (FilesystemException e) {
         throw new LoggingException($"Failed to create/open log file \"{path}\": {e.Reason}", e);
     }
     FilePath = path;
 }
Beispiel #5
0
        /// <summary>
        /// Logs a message to the associated file.
        /// </summary>
        /// <param name="level">The level of the message.</param>
        /// <param name="message">The message to log.</param>
        /// <exception cref="LoggingException">If the message could not be written to the file due to I/O errors.
        /// </exception>
        public void Log(LogLevel level, string message)
        {
            var formattedMessage = LogFormatter.FormatMessage(level, message);

            try {
                FilesystemException.ConvertSystemException(() => {
                    Stream.Write(formattedMessage);
                    Stream.Flush();
                }, () => FilePath);
            }
            catch (FilesystemException e) {
                throw new LoggingException($"Failed to log to file \"{FilePath}\": {e.Reason}", e);
            }
        }
Beispiel #6
0
        /// <summary>
        /// Reads a backup index from file.
        /// </summary>
        /// <param name="filePath">The path to the index file.</param>
        /// <returns>The read backup index.</returns>
        /// <exception cref="BackupIndexFileIOException">If a filesystem error occurs during reading.</exception>
        /// <exception cref="BackupIndexFileParseException">If the file is not a valid backup index.</exception>
        public static BackupIndex Read(string filePath)
        {
            using var stream = OpenFile(filePath);

            BackupIndex index   = new();
            long        lineNum = 0;

            while (true)
            {
                string?line;
                try {
                    line = FilesystemException.ConvertSystemException(() => stream.ReadLine(), () => filePath);
                }
                catch (FilesystemException e) {
                    throw new BackupIndexFileIOException(filePath, e);
                }

                if (line is null)
                {
                    break;
                }
                lineNum++;

                // Empty line ok, just skip it.
                if (line.Length == 0)
                {
                    continue;
                }

                // Split on first separator (should be exactly 1).
                var parts = line.Split(BackupIndexFileConstants.SEPARATOR, 2);
                if (parts.Length != 2)
                {
                    throw new BackupIndexFileParseException(filePath, lineNum);
                }

                // Note that empty values are valid, even though that shouldn't happen in practice.
                var backupDirectory  = parts[0];
                var backupSourcePath = Utility.NewlineDecode(parts[1]);

                index.Backups[backupDirectory] = backupSourcePath;
            }

            return(index);
        }
        /// <summary>
        /// Performs the back up. <br/>
        /// Should not be called more than once per instance.
        /// </summary>
        /// <returns>The results of the backup.</returns>
        /// <exception cref="BackupServiceException">If the source directory can't be accessed.</exception>
        private BackupResults Run()
        {
            // Explore directories in a depth-first manner, on the basis that files/directories within the same branch
            // of the filesystem are likely to be modified together, so we want to back them up as close together in
            // time as possible. It's also probably more useful to have some directories fully backed up rather than
            // all directories partially backed up (if using breadth-first), in the case the backup is stopped early.

            SearchState searchState = new(true, new(1000), new(20));

            try {
                var root = FilesystemException.ConvertSystemException(() => new DirectoryInfo(SourcePath),
                                                                      () => SourcePath);
                searchState.NodeStack.Add(state => VisitDirectory(state, root));
            }
            catch (FilesystemException e) {
                throw new BackupServiceException($"Failed to enumerate source directory \"{SourcePath}\": {e.Reason}", e);
            }

            do
            {
                var currentNode = searchState.NodeStack[^ 1];
Beispiel #8
0
        /// <summary>
        /// Writes a new backup entry to a backup index file.
        /// </summary>
        /// <param name="indexFilePath">The path of the index file to write to. May be a nonexistent file, in which
        /// case it will be created.</param>
        /// <param name="backupName">The name of the backup directory. Must not contain
        /// <see cref="BackupIndexFileConstants.SEPARATOR"/> or newline characters.</param>
        /// <param name="backupSourcePath">The path of the source directory for the backup.</param>
        /// <exception cref="ArgumentException">If <paramref name="backupName"/> contains invalid characters.
        /// </exception>
        /// <exception cref="BackupIndexFileIOException">Failed to write to the index file due to filesystem-related
        /// errors.</exception>
        public static void AddEntry(string indexFilePath, string backupName, string backupSourcePath)
        {
            if (backupName.Contains(BackupIndexFileConstants.SEPARATOR))
            {
                throw new ArgumentException(
                          $"{nameof(backupName)} must not contain {BackupIndexFileConstants.SEPARATOR}", nameof(backupName));
            }
            if (Utility.ContainsNewlines(backupName))
            {
                throw new ArgumentException($"{nameof(backupName)} must not contain newlines.", nameof(backupName));
            }

            var entry = $"{backupName}{BackupIndexFileConstants.SEPARATOR}{Utility.NewlineEncode(backupSourcePath)}\n";

            try {
                FilesystemException.ConvertSystemException(
                    () => File.AppendAllText(indexFilePath, entry, new UTF8Encoding(false, true)),
                    () => indexFilePath);
            }
            catch (FilesystemException e) {
                throw new BackupIndexFileIOException(indexFilePath, e);
            }
        }