public override void ExecuteBuild() { string BucketName = ParseRequiredStringParam("Bucket"); FileReference CredentialsFile = ParseRequiredFileReferenceParam("CredentialsFile"); string CredentialsKey = ParseRequiredStringParam("CredentialsKey"); DirectoryReference CacheDir = ParseRequiredDirectoryReferenceParam("CacheDir"); DirectoryReference FilterDir = ParseRequiredDirectoryReferenceParam("FilterDir"); int Days = ParseParamInt("Days", 7); int MaxFileSize = ParseParamInt("MaxFileSize", 0); string RootManifestPath = ParseRequiredStringParam("Manifest"); string KeyPrefix = ParseParamValue("KeyPrefix", ""); bool bReset = ParseParam("Reset"); // The credentials to upload with AWSCredentials Credentials; // Try to get the credentials by the key passed in from the script CredentialProfileStoreChain CredentialsChain = new CredentialProfileStoreChain(CredentialsFile.FullName); if (!CredentialsChain.TryGetAWSCredentials(CredentialsKey, out Credentials)) { throw new AutomationException("Unknown credentials key: {0}", CredentialsKey); } // Create the new client using (AmazonS3Client Client = new AmazonS3Client(Credentials, Region)) { using (SemaphoreSlim RequestSemaphore = new SemaphoreSlim(4)) { // Read the filters HashSet <string> Paths = new HashSet <string>(); foreach (FileInfo FilterFile in FilterDir.ToDirectoryInfo().EnumerateFiles("*.txt")) { TimeSpan Age = DateTime.UtcNow - FilterFile.LastWriteTimeUtc; if (Age < TimeSpan.FromDays(3)) { Log.TraceInformation("Reading {0}", FilterFile.FullName); string[] Lines = File.ReadAllLines(FilterFile.FullName); foreach (string Line in Lines) { string TrimLine = Line.Trim().Replace('\\', '/'); if (TrimLine.Length > 0) { Paths.Add(TrimLine); } } } else if (Age > TimeSpan.FromDays(5)) { try { Log.TraceInformation("Deleting {0}", FilterFile.FullName); FilterFile.Delete(); } catch (Exception Ex) { Log.TraceWarning("Unable to delete: {0}", Ex.Message); Log.TraceLog(ExceptionUtils.FormatExceptionDetails(Ex)); } } } Log.TraceInformation("Found {0:n0} files", Paths.Count); // Enumerate all the files that are in the network DDC Log.TraceInformation(""); Log.TraceInformation("Filtering files in {0}...", CacheDir); List <DerivedDataFile> Files = ParallelExecute <string, DerivedDataFile>(Paths, (Path, FilesBag) => ReadFileInfo(CacheDir, Path, FilesBag)); // Filter to the maximum size if (MaxFileSize != 0) { int NumRemovedMaxSize = Files.RemoveAll(x => x.Info.Length > MaxFileSize); Log.TraceInformation(""); Log.TraceInformation("Removed {0} files above size limit ({1:n0} bytes)", NumRemovedMaxSize, MaxFileSize); } // Create the working directory DirectoryReference WorkingDir = DirectoryReference.Combine(EngineDirectory, "Saved", "UploadDDC"); DirectoryReference.CreateDirectory(WorkingDir); // Get the path to the manifest FileReference RootManifestFile = FileReference.Combine(CommandUtils.RootDirectory, RootManifestPath); // Read the old root manifest RootManifest OldRootManifest = new RootManifest(); if (FileReference.Exists(RootManifestFile)) { OldRootManifest.Read(JsonObject.Read(RootManifestFile)); } // Read the old bundle manifest BundleManifest OldBundleManifest = new BundleManifest(); if (OldRootManifest.Entries.Count > 0) { FileReference LocalBundleManifest = FileReference.Combine(WorkingDir, "OldBundleManifest.json"); if (TryDownloadFile(Client, BucketName, OldRootManifest.Entries.Last().Key, LocalBundleManifest)) { OldBundleManifest.Read(JsonObject.Read(LocalBundleManifest)); } } // Create the new manifest BundleManifest NewBundleManifest = new BundleManifest(); // Try to download the old manifest, and add all the bundles we want to keep to the new manifest if (!bReset) { foreach (BundleManifest.Entry Bundle in OldBundleManifest.Entries) { FileReference BundleFile = FileReference.Combine(WorkingDir, Bundle.Name); if (!FileReference.Exists(BundleFile)) { Log.TraceInformation("Downloading {0}", BundleFile); FileReference TempCompressedFile = new FileReference(BundleFile.FullName + ".incoming.gz"); if (!TryDownloadFile(Client, BucketName, Bundle.ObjectKey, TempCompressedFile)) { Log.TraceWarning("Unable to download {0}", Bundle.ObjectKey); continue; } FileReference TempUncompressedFile = new FileReference(BundleFile.FullName + ".incoming"); try { DecompressFile(TempCompressedFile, TempUncompressedFile); } catch (Exception Ex) { Log.TraceWarning("Unable to uncompress {0}: {1}", Bundle.ObjectKey, Ex.ToString()); continue; } FileReference.Move(TempUncompressedFile, BundleFile); } NewBundleManifest.Entries.Add(Bundle); } } // Figure out all the item digests that we already have Dictionary <BundleManifest.Entry, HashSet <ContentHash> > BundleToKeyHashes = new Dictionary <BundleManifest.Entry, HashSet <ContentHash> >(); foreach (BundleManifest.Entry Bundle in NewBundleManifest.Entries) { HashSet <ContentHash> KeyHashes = new HashSet <ContentHash>(); FileReference BundleFile = FileReference.Combine(WorkingDir, Bundle.Name); using (FileStream Stream = FileReference.Open(BundleFile, FileMode.Open, FileAccess.Read, FileShare.Read)) { BinaryReader Reader = new BinaryReader(Stream); uint Signature = Reader.ReadUInt32(); if (Signature != BundleSignatureV1) { throw new Exception(String.Format("Invalid signature for {0}", BundleFile)); } int NumEntries = Reader.ReadInt32(); for (int EntryIdx = 0; EntryIdx < NumEntries; EntryIdx++) { byte[] Digest = new byte[ContentHash.LengthSHA1]; if (Reader.Read(Digest, 0, ContentHash.LengthSHA1) != ContentHash.LengthSHA1) { throw new Exception("Unexpected EOF"); } KeyHashes.Add(new ContentHash(Digest)); Stream.Seek(4, SeekOrigin.Current); } } BundleToKeyHashes[Bundle] = KeyHashes; } // Calculate the download size of the manifest long DownloadSize = NewBundleManifest.Entries.Sum(x => (long)x.CompressedLength); // Remove any bundles which have less than the minimum required size in valid data. We don't mark the manifest as dirty yet; these // files will only be rewritten if new content is added, to prevent the last bundle being rewritten multiple times. foreach (KeyValuePair <BundleManifest.Entry, HashSet <ContentHash> > Pair in BundleToKeyHashes) { long ValidBundleSize = Files.Where(x => Pair.Value.Contains(x.KeyHash)).Sum(x => (long)x.Info.Length); if (ValidBundleSize < MinBundleSize) { NewBundleManifest.Entries.Remove(Pair.Key); } } // Find all the valid digests HashSet <ContentHash> ReusedKeyHashes = new HashSet <ContentHash>(); foreach (BundleManifest.Entry Bundle in NewBundleManifest.Entries) { ReusedKeyHashes.UnionWith(BundleToKeyHashes[Bundle]); } // Remove all the files which already exist int NumRemovedExist = Files.RemoveAll(x => ReusedKeyHashes.Contains(x.KeyHash)); if (NumRemovedExist > 0) { Log.TraceInformation(""); Log.TraceInformation("Removed {0:n0} files which already exist", NumRemovedExist); } // Read all the files we want to include List <Tuple <DerivedDataFile, byte[]> > FilesToInclude = new List <Tuple <DerivedDataFile, byte[]> >(); if (Files.Count > 0) { Log.TraceInformation(""); Log.TraceInformation("Reading remaining {0:n0} files into memory ({1:n1}mb)...", Files.Count, (float)Files.Sum(x => (long)x.Info.Length) / (1024 * 1024)); FilesToInclude.AddRange(ParallelExecute <DerivedDataFile, Tuple <DerivedDataFile, byte[]> >(Files, (x, y) => ReadFileData(x, y))); } // Generate new data using (RNGCryptoServiceProvider Crypto = new RNGCryptoServiceProvider()) { // Flag for whether to update the manifest bool bUpdateManifest = false; // Upload the new bundle Log.TraceInformation(""); if (FilesToInclude.Count == 0) { Log.TraceInformation("No new files to add."); } else { // Sort the files to include by creation time. This will bias towards grouping older, more "permanent", items together. Log.TraceInformation("Sorting input files"); List <Tuple <DerivedDataFile, byte[]> > SortedFilesToInclude = FilesToInclude.OrderBy(x => x.Item1.Info.CreationTimeUtc).ToList(); // Get the target bundle size long TotalSize = SortedFilesToInclude.Sum(x => (long)x.Item2.Length); int NumBundles = (int)((TotalSize + (MaxBundleSize - 1)) / MaxBundleSize); long TargetBundleSize = TotalSize / NumBundles; // Split the input data into bundles List <List <Tuple <DerivedDataFile, byte[]> > > BundleFilesToIncludeList = new List <List <Tuple <DerivedDataFile, byte[]> > >(); long BundleSize = 0; for (int FileIdx = 0; FileIdx < SortedFilesToInclude.Count; BundleSize = BundleSize % TargetBundleSize) { List <Tuple <DerivedDataFile, byte[]> > BundleFilesToInclude = new List <Tuple <DerivedDataFile, byte[]> >(); for (; BundleSize < TargetBundleSize && FileIdx < SortedFilesToInclude.Count; FileIdx++) { BundleFilesToInclude.Add(SortedFilesToInclude[FileIdx]); BundleSize += SortedFilesToInclude[FileIdx].Item2.Length; } BundleFilesToIncludeList.Add(BundleFilesToInclude); } // Upload each bundle DateTime NewBundleTime = DateTime.UtcNow; for (int BundleIdx = 0; BundleIdx < BundleFilesToIncludeList.Count; BundleIdx++) { List <Tuple <DerivedDataFile, byte[]> > BundleFilesToInclude = BundleFilesToIncludeList[BundleIdx]; // Get the new bundle info string NewBundleSuffix = (BundleFilesToIncludeList.Count > 1) ? String.Format("-{0}_of_{1}", BundleIdx + 1, BundleFilesToIncludeList.Count) : ""; string NewBundleName = String.Format("Bundle-{0:yyyy.MM.dd-HH.mm}{1}.ddb", NewBundleTime.ToLocalTime(), NewBundleSuffix); // Create a random number for the object key string NewBundleObjectKey = KeyPrefix + "bulk/" + CreateObjectName(Crypto); // Create the bundle header byte[] Header; using (MemoryStream HeaderStream = new MemoryStream()) { BinaryWriter Writer = new BinaryWriter(HeaderStream); Writer.Write(BundleSignatureV1); Writer.Write(BundleFilesToInclude.Count); foreach (Tuple <DerivedDataFile, byte[]> FileToInclude in BundleFilesToInclude) { Writer.Write(FileToInclude.Item1.KeyHash.Bytes, 0, ContentHash.LengthSHA1); Writer.Write((int)FileToInclude.Item2.Length); } Header = HeaderStream.ToArray(); } // Create the output file FileReference NewBundleFile = FileReference.Combine(WorkingDir, NewBundleName + ".gz"); Log.TraceInformation("Writing {0}", NewBundleFile); using (FileStream BundleStream = FileReference.Open(NewBundleFile, FileMode.Create, FileAccess.Write, FileShare.Read)) { using (GZipStream ZipStream = new GZipStream(BundleStream, CompressionLevel.Optimal, true)) { ZipStream.Write(Header, 0, Header.Length); foreach (Tuple <DerivedDataFile, byte[]> FileToInclude in BundleFilesToInclude) { ZipStream.Write(FileToInclude.Item2, 0, FileToInclude.Item2.Length); } } } // Upload the file long NewBundleCompressedLength = NewBundleFile.ToFileInfo().Length; long NewBundleUncompressedLength = Header.Length + BundleFilesToInclude.Sum(x => (long)x.Item2.Length); Log.TraceInformation("Uploading bundle to {0} ({1:n1}mb)", NewBundleObjectKey, NewBundleCompressedLength / (1024.0f * 1024.0f)); UploadFile(Client, BucketName, NewBundleFile, 0, NewBundleObjectKey, RequestSemaphore, null); // Add the bundle to the new manifest BundleManifest.Entry Bundle = new BundleManifest.Entry(); Bundle.Name = NewBundleName; Bundle.ObjectKey = NewBundleObjectKey; Bundle.Time = NewBundleTime; Bundle.CompressedLength = (int)NewBundleCompressedLength; Bundle.UncompressedLength = (int)NewBundleUncompressedLength; NewBundleManifest.Entries.Add(Bundle); // Mark the manifest as requiring an update bUpdateManifest = true; } } // Update the manifest if (bUpdateManifest) { DateTime UtcNow = DateTime.UtcNow; DateTime RemoveBundleManifestsBefore = UtcNow - TimeSpan.FromDays(3.0); // Update the root manifest RootManifest NewRootManifest = new RootManifest(); NewRootManifest.AccessKey = OldRootManifest.AccessKey; NewRootManifest.SecretKey = OldRootManifest.SecretKey; foreach (RootManifest.Entry Entry in OldRootManifest.Entries) { if (Entry.CreateTime >= RemoveBundleManifestsBefore) { NewRootManifest.Entries.Add(Entry); } } // Make sure there's an entry for the last 24h DateTime RequireBundleManifestAfter = UtcNow - TimeSpan.FromDays(1.0); if (!NewRootManifest.Entries.Any(x => x.CreateTime > RequireBundleManifestAfter)) { RootManifest.Entry NewEntry = new RootManifest.Entry(); NewEntry.CreateTime = UtcNow; NewEntry.Key = KeyPrefix + CreateObjectName(Crypto); NewRootManifest.Entries.Add(NewEntry); } // Save out the new bundle manifest FileReference NewBundleManifestFile = FileReference.Combine(WorkingDir, "NewBundleManifest.json"); NewBundleManifest.Save(NewBundleManifestFile); // Update all the bundle manifests still valid foreach (RootManifest.Entry Entry in NewRootManifest.Entries) { Log.TraceInformation("Uploading bundle manifest to {0}", Entry.Key); UploadFile(Client, BucketName, NewBundleManifestFile, 0, Entry.Key, RequestSemaphore, null); } // Overwrite all the existing manifests if (AllowSubmit) { List <string> ExistingFiles = P4.Files(CommandUtils.MakePathSafeToUseWithCommandLine(RootManifestFile.FullName)); // Create a changelist containing the new manifest int ChangeNumber = P4.CreateChange(Description: "Updating DDC bundle manifest"); if (ExistingFiles.Count > 0) { P4.Edit(ChangeNumber, CommandUtils.MakePathSafeToUseWithCommandLine(RootManifestFile.FullName)); NewRootManifest.Save(RootManifestFile); } else { NewRootManifest.Save(RootManifestFile); P4.Add(ChangeNumber, CommandUtils.MakePathSafeToUseWithCommandLine(RootManifestFile.FullName)); } // Submit it int SubmittedChangeNumber; P4.Submit(ChangeNumber, out SubmittedChangeNumber, true); if (SubmittedChangeNumber <= 0) { throw new AutomationException("Failed to submit change"); } // Delete any bundles that are no longer referenced HashSet <string> KeepObjectKeys = new HashSet <string>(NewBundleManifest.Entries.Select(x => x.ObjectKey)); foreach (BundleManifest.Entry OldEntry in OldBundleManifest.Entries) { if (!KeepObjectKeys.Contains(OldEntry.ObjectKey)) { Log.TraceInformation("Deleting unreferenced bundle {0}", OldEntry.ObjectKey); DeleteFile(Client, BucketName, OldEntry.ObjectKey, RequestSemaphore, null); } } // Delete any bundle manifests which are no longer referenced HashSet <string> KeepManifestKeys = new HashSet <string>(NewRootManifest.Entries.Select(x => x.Key)); foreach (RootManifest.Entry OldEntry in OldRootManifest.Entries) { if (!KeepManifestKeys.Contains(OldEntry.Key)) { Log.TraceInformation("Deleting unreferenced manifest {0}", OldEntry.Key); DeleteFile(Client, BucketName, OldEntry.Key, RequestSemaphore, null); } } } else { // Skip submitting Log.TraceWarning("Skipping manifest submit due to missing -Submit argument."); } // Update the new download size DownloadSize = NewBundleManifest.Entries.Sum(x => (long)x.CompressedLength); } } // Print some stats about the final manifest Log.TraceInformation(""); Log.TraceInformation("Total download size {0:n1}mb", DownloadSize / (1024.0 * 1024.0)); Log.TraceInformation(""); } } }