Esempio n. 1
0
        /// <summary>
        /// Generates package content differentials between the source and target snapshots in the specified output path. Note that additional package modifications may be required afterwards to account for content not included in the snapshots.
        /// </summary>
        /// <param name="repo">The initial repository configuration.</param>
        /// <param name="sourcePath">The absolute local source content path.</param>
        /// <param name="targetPath">The absolute localtarget content path.</param>
        /// <param name="deltaPath">The delta content path; these files should be uploaded to the repo.</param>
        /// <param name="settings">The optional package settings.</param>
        /// <param name="progressHandler">The optional handler to report progress to.</param>
        /// <param name="progressStatus">The optional progress status description.</param>
        /// <returns>The created package information.</returns>
        public Package(Repository repo, Path sourcePath, Path targetPath, Path deltaPath, PackageSettings settings = null, ProgressChangedEventHandler progressHandler = null, string progressStatus = null)
        {
            Repository = repo ?? throw new ArgumentNullException(nameof(repo));

            if (!Directory.Exists(sourcePath))
            {
                throw new DirectoryNotFoundException("Source path does not exist.");
            }

            if (!Directory.Exists(targetPath))
            {
                throw new DirectoryNotFoundException("Target path does not exist.");
            }

            if (!Directory.Exists(deltaPath))
            {
                throw new DirectoryNotFoundException("Delta path does not exist.");
            }

            // use the specified settings or initialize with defaults if null
            PackageSettings pkgSettings = settings ?? new PackageSettings();

            // generate the empty package and create it's content directory based on its ID
            Path packagePath = Path.Combine(deltaPath, Id + "\\");

            Directory.CreateDirectory(packagePath);

            // TODO: automatic detection of relative versus absolute paths (absolute should override and not use base path if specified)

            try
            {
                #region Snapshots

                // generate source snapshot, use existing one in repo if content matches
                var sourceSnapshot = new Snapshot(sourcePath, progressHandler, "Generating source snapshot");
                if (repo.Snapshots.Contains(sourceSnapshot))
                {
                    sourceSnapshot = repo.Snapshots[repo.Snapshots.IndexOf(sourceSnapshot)];
                }
                else
                {
                    repo.Snapshots.Add(sourceSnapshot);
                }

                // generate target snapshot, use existing one in repo if content matches
                var targetSnapshot = new Snapshot(targetPath, progressHandler, "Generating target snapshot");
                if (repo.Snapshots.Contains(targetSnapshot))
                {
                    targetSnapshot = repo.Snapshots[repo.Snapshots.IndexOf(targetSnapshot)];
                }
                else
                {
                    repo.Snapshots.Add(targetSnapshot);
                }

                // abort if package already exists
                if (repo.Packages.Any(pkg => Equals(pkg.SourceSnapshot, sourceSnapshot) && Equals(pkg.TargetSnapshot, targetSnapshot)))
                {
                    throw new NotSupportedException("Package already exists for the specified source and target content.");
                }

                // link snapshots to the package
                SourceSnapshot = sourceSnapshot;
                TargetSnapshot = targetSnapshot;

                #endregion

                #region Lookup Tables

                // generate lookup tables
                var sourcePaths  = new Dictionary <Path, FileInformation>();
                var sourceFiles  = new Dictionary <Path, List <FileInformation> >();
                var sourceHashes = new Dictionary <string, List <FileInformation> >();
                foreach (var info in sourceSnapshot.Files)
                {
                    sourcePaths[info.Path] = info;

                    Path fileName = Path.GetFileName(info.Path);
                    if (!sourceFiles.ContainsKey(fileName))
                    {
                        sourceFiles[fileName] = new List <FileInformation>();
                    }
                    sourceFiles[fileName].Add(info);

                    if (info.Hash != null)
                    {
                        var hash = BitConverter.ToString(info.Hash);
                        if (!sourceHashes.ContainsKey(hash))
                        {
                            sourceHashes[hash] = new List <FileInformation>();
                        }
                        sourceHashes[hash].Add(info);
                    }
                }

                var targetPaths  = new Dictionary <Path, FileInformation>();
                var targetFiles  = new Dictionary <Path, List <FileInformation> >();
                var targetHashes = new Dictionary <string, List <FileInformation> >();
                foreach (var info in targetSnapshot.Files)
                {
                    targetPaths[info.Path] = info;

                    Path fileName = Path.GetFileName(info.Path);
                    if (!targetFiles.ContainsKey(fileName))
                    {
                        targetFiles[fileName] = new List <FileInformation>();
                    }
                    targetFiles[fileName].Add(info);

                    if (info.Hash != null)
                    {
                        var hash = BitConverter.ToString(info.Hash);
                        if (!targetHashes.ContainsKey(hash))
                        {
                            targetHashes[hash] = new List <FileInformation>();
                        }
                        targetHashes[hash].Add(info);
                    }
                }

                #endregion

                #region Package Content Generation

                progressHandler?.Invoke(this, new ProgressChangedEventArgs(0, progressStatus));

                // TODO: no good way to get regular progress callbacks from all patchers so just count files processed instead for now
                int processedCount = 0;

                Parallel.ForEach(targetSnapshot.Files, file =>
                {
                    Path sourceFilePath  = Path.Combine(sourcePath, file.Path);
                    Path targetFilePath  = Path.Combine(targetPath, file.Path);
                    bool sourcePathMatch = sourcePaths.ContainsKey(file.Path);

                    // only applicable for files
                    string hash                = !file.Path.IsDirectory && file.Hash != null ? BitConverter.ToString(file.Hash) : null;
                    bool sourceHashMatch       = !file.Path.IsDirectory && file.Hash != null ? sourceHashes.ContainsKey(hash) : false;
                    bool sourceHashSingleMatch = !file.Path.IsDirectory && (sourceHashMatch && sourceHashes[hash].Count == 1);
                    bool targetHashSingleMatch = !file.Path.IsDirectory && file.Hash != null ? targetHashes[hash].Count == 1 : false;

                    // unchanged empty directory
                    if (file.Path.IsDirectory & sourcePathMatch)
                    {
                        // do nothing
                    }

                    // added empty directory
                    else if (file.Path.IsDirectory && !sourcePathMatch)
                    {
                        lock (((ICollection)Actions).SyncRoot)
                        {
                            Actions.Add(new AddAction(file.Path));
                        }
                    }

                    // search for added optional files (target file with a null hash)
                    else if (!file.Path.IsDirectory && file.Hash == null)
                    {
                        var content = new Content(targetFilePath, packagePath, pkgSettings);

                        lock (((ICollection)Actions).SyncRoot)
                        {
                            Actions.Add(new AddAction(file.Path, content, false, true));
                        }
                    }

                    // unchanged file
                    else if (!file.Path.IsDirectory && sourcePathMatch && sourceHashMatch)
                    {
                        // do nothing
                    }

                    // changed files (same path, different hash)
                    else if (!file.Path.IsDirectory && sourcePathMatch && !sourceHashMatch)
                    {
                        Path tempDeltaFile = Utility.GetTempFilePath();

                        try
                        {
                            // patches should already be compressed, no need to do so again
                            PackageSettings pkgSettingsCopy    = (PackageSettings)pkgSettings.Clone(); // deep clone for thread safety
                            pkgSettingsCopy.CompressionEnabled = false;

                            // generate delta patch to temp location and generate content
                            Utility.GetPatcher(pkgSettingsCopy.PatchAlgorithmType).Create(sourceFilePath, targetFilePath, tempDeltaFile);
                            var content = new Content(tempDeltaFile, packagePath, pkgSettingsCopy);
                            lock (((ICollection)Actions).SyncRoot)
                            {
                                Actions.Add(new PatchAction(file.Path, file.Path, pkgSettingsCopy.PatchAlgorithmType, content));
                            }
                        }
                        finally
                        {
                            File.Delete(tempDeltaFile);
                        }
                    }

                    // moved files (different path, single hash match on both sides)
                    else if (!file.Path.IsDirectory && !sourcePathMatch && sourceHashSingleMatch && targetHashSingleMatch)
                    {
                        lock (((ICollection)Actions).SyncRoot)
                        {
                            Actions.Add(new MoveAction(sourceHashes[hash][0].Path, file.Path));
                        }
                    }

                    // search for copied files (multiple hash matches, different paths)
                    else if (!file.Path.IsDirectory && !sourcePathMatch && sourceHashMatch)
                    {
                        lock (((ICollection)Actions).SyncRoot)
                        {
                            Actions.Add(new CopyAction(sourceHashes[hash][0].Path, file.Path));
                        }
                    }

                    // search for added files (path and hash that exists in target but not source)
                    else if (!file.Path.IsDirectory && !sourcePathMatch && !sourceHashMatch)
                    {
                        var content = new Content(targetFilePath, packagePath, pkgSettings);

                        lock (((ICollection)Actions).SyncRoot)
                        {
                            Actions.Add(new AddAction(file.Path, content));
                        }
                    }

                    else
                    {
                        throw new InvalidOperationException("File action not found.");
                    }

                    // propagate current progress upstream
                    int count = Interlocked.Increment(ref processedCount);
                    int progressPercentage = (int)(count / (float)targetSnapshot.Files.Count * 100);
                    progressHandler?.Invoke(this, new ProgressChangedEventArgs(progressPercentage, progressStatus));
                });

                // deleted files & directories (path that exists in source but not target)
                foreach (var file in sourceSnapshot.Files)
                {
                    if (!targetPaths.ContainsKey(file.Path))
                    {
                        Actions.Add(new RemoveAction(file.Path, true));
                    }
                }

                // TODO: handle empty directory deletions caused by files getting deleted within
                // loop through destination directories and delete any that don't exist in source

                // TODO: handle file/directory renames that consist of case-changes only
                // loop through all files (should be 1:1 now) and rename if case mismatches (implies underlying directories are also renamed where applicable)

                #endregion
            }
            catch (Exception ex)
            {
                Directory.Delete(packagePath, true);
            }
        }