public void AddDirectoryContents_GivenSubfolder_WritesContentAtExpectedPath() { /////////// // Setup // /////////// string fileContents = "Hello."; // When disposing, deletes the temporary folder including its contents. using (var testFolder = new TemporaryFolder(kTestFolderName)) { var subFolder = new TemporaryFolder(testFolder, "subFolder"); var testFilePath = Path.Combine(subFolder.Path, Path.ChangeExtension(Path.GetRandomFileName(), ".txt")); RobustFile.WriteAllText(testFilePath, fileContents); // This is not under the test folder. It makes creating the archive less thorny that way. make sure it gets cleaned up. using (var archiveFile = TempFile.WithExtension(".tar")) { /////////////////////// // System under test // /////////////////////// var archive = new BloomTarArchive(archiveFile.Path); archive.AddDirectoryContents(testFolder.Path); archive.Save(); ////////////////// // Verification // ////////////////// var inStream = RobustFile.OpenRead(archiveFile.Path); TarArchive archiveForReading = TarArchive.CreateInputTarArchive(inStream, Encoding.UTF8); using (var extractedFolder = new TemporaryFolder(testFolder, "extractedTarContents")) { archiveForReading.ExtractContents(extractedFolder.Path, false); string expectedFilePath = Path.Combine(extractedFolder.Path, "subFolder", Path.GetFileName(testFilePath)); FileAssert.Exists(expectedFilePath); FileAssert.AreEqual(expectedFilePath, testFilePath, "Incorrect file contents."); archiveForReading.Close(); } } ///////////// // Cleanup // ///////////// // Nothing else necessary, disposing the TemporaryFolder cleans up all its subcontents. } }
private void AddFile(string path, string entryName) { var fi = new FileInfo(path); var newEntry = new ZipEntry(entryName) { DateTime = fi.LastWriteTime, Size = fi.Length, IsUnicodeText = true }; _zipStream.PutNextEntry(newEntry); // Zip the file in buffered chunks var buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(path)) { StreamUtils.Copy(streamReader, _zipStream, buffer); } _zipStream.CloseEntry(); }
protected override void AddFile(string path, string entryName, bool compressIfAble = true) { var fi = new FileInfo(path); var newEntry = new ZipEntry(entryName) { DateTime = fi.LastWriteTime, Size = fi.Length, IsUnicodeText = true, CompressionMethod = compressIfAble?CompressionMethod.Deflated:CompressionMethod.Stored }; _zipStream.PutNextEntry(newEntry); // Zip the file in buffered chunks var buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(path)) { StreamUtils.Copy(streamReader, _zipStream, buffer); } _zipStream.CloseEntry(); }
/// <summary> /// Adds a file to the archive /// </summary> /// <param name="compressIfAble">This parameter is ignored because tar files don't support compression. It is recommended to pass "false" though.</param> protected override void AddFile(string path, string entryName, bool compressIfAble = true) { var fi = new FileInfo(path); TarEntry newEntry = TarEntry.CreateEntryFromFile(path); newEntry.TarHeader.Name = entryName; newEntry.Size = fi.Length; _tarStream.PutNextEntry(newEntry); // Add to the archive in buffered chunks var buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(path)) { StreamUtils.Copy(streamReader, _tarStream, buffer); } _tarStream.CloseEntry(); }
public void ReplyWithFileContent(string path, string originalPath = null) { //Deal with BL-3153, where the file was still open in another thread FileStream fs; if (!RobustFile.Exists(path)) { //for audio, at least, this is not really an error. We constantly are asking if audio already exists for the current segment //enhance: maybe audio should go through a different path, e.g. "/bloom/audio/somefile.wav" //then this path COULD write and error //Logger.WriteError("Server could not find" + path); _actualContext.Response.StatusCode = 404; return; } try { fs = RobustFile.OpenRead(path); } catch (Exception error) { Logger.WriteError("Server could not read " + path, error); _actualContext.Response.StatusCode = 500; return; } using (fs) { _actualContext.Response.ContentLength64 = fs.Length; _actualContext.Response.AppendHeader("PathOnDisk", HttpUtility.UrlEncode(path)); if (ShouldCache(path, originalPath)) { _actualContext.Response.AppendHeader("Cache-Control", "max-age=600000"); // about a week...if someone spends longer editing one book, well, files will get loaded one more time... } // A HEAD request (rather than a GET or POST request) is a request for just headers, and nothing can be written // to the OutputStream. It is normally used to check if the contents of the file have changed without taking the // time and bandwidth needed to download the full contents of the file. The 2 pieces of information being returned // are the Content-Length and Last-Modified headers. The requestor can use this information to determine if the // contents of the file have changed, and if they have changed the requestor can then decide if the file needs to // be reloaded. It is useful when debugging with tools which automatically reload the page when something changes. if (_actualContext.Request.HttpMethod == "HEAD") { var lastModified = RobustFile.GetLastWriteTimeUtc(path).ToString("R"); // Originally we were returning the Last-Modified header with every response, but we discovered that this was // causing Geckofx to cache the contents of the files. This made debugging difficult because, even if the file // changed, Geckofx would use the cached file rather than requesting the updated file from the localhost. _actualContext.Response.AppendHeader("Last-Modified", lastModified); } else if (fs.Length < 2 * 1024 * 1024) { // This buffer size was picked to be big enough for any of the standard files we load in every page. // Profiling indicates it is MUCH faster to use Response.Close() rather than writing to the output stream, // though the gain may be illusory since the final 'false' argument allows our code to proceed without waiting // for the complete data transfer. At a minimum, it makes this thread available to work on another // request sooner. var buffer = new byte[fs.Length]; fs.Read(buffer, 0, (int)fs.Length); _actualContext.Response.Close(buffer, false); } else { // For really big (typically image) files, use the old buffered approach try { var buffer = new byte[1024 * 512]; //512KB int read; while ((read = fs.Read(buffer, 0, buffer.Length)) > 0) { _actualContext.Response.OutputStream.Write(buffer, 0, read); } _actualContext.Response.OutputStream.Close(); } catch (HttpListenerException e) { // If the page is gone and no longer able to accept the data, just log it. ReportHttpListenerProblem(e); } } } HaveOutput = true; }
public void ReplyWithFileContent(string path) { //Deal with BL-3153, where the file was still open in another thread FileStream fs; if (!RobustFile.Exists(path)) { //for audio, at least, this is not really an error. We constantly are asking if audio already exists for the current segment //enhance: maybe audio should go through a different path, e.g. "/bloom/audio/somefile.wav" //then this path COULD write and error //Logger.WriteError("Server could not find" + path); _actualContext.Response.StatusCode = 404; return; } try { fs = RobustFile.OpenRead(path); } catch (Exception error) { Logger.WriteError("Server could not read " + path, error); _actualContext.Response.StatusCode = 500; return; } using (fs) { _actualContext.Response.ContentLength64 = fs.Length; _actualContext.Response.AppendHeader("PathOnDisk", HttpUtility.UrlEncode(path)); //helps with debugging what file is being chosen // A HEAD request (rather than a GET or POST request) is a request for just headers, and nothing can be written // to the OutputStream. It is normally used to check if the contents of the file have changed without taking the // time and bandwidth needed to download the full contents of the file. The 2 pieces of information being returned // are the Content-Length and Last-Modified headers. The requestor can use this information to determine if the // contents of the file have changed, and if they have changed the requestor can then decide if the file needs to // be reloaded. It is useful when debugging with tools which automatically reload the page when something changes. if (_actualContext.Request.HttpMethod == "HEAD") { var lastModified = RobustFile.GetLastWriteTimeUtc(path).ToString("R"); // Originally we were returning the Last-Modified header with every response, but we discovered that this was // causing Geckofx to cache the contents of the files. This made debugging difficult because, even if the file // changed, Geckofx would use the cached file rather than requesting the updated file from the localhost. _actualContext.Response.AppendHeader("Last-Modified", lastModified); } else { var buffer = new byte[1024 * 512]; //512KB int read; while ((read = fs.Read(buffer, 0, buffer.Length)) > 0) { _actualContext.Response.OutputStream.Write(buffer, 0, read); } } } _actualContext.Response.OutputStream.Close(); HaveOutput = true; }
/// <summary> /// Get a json of stats about the image. It is used to populate a tooltip when you hover over an image container /// </summary> private void HandleImageInfo(ApiRequest request) { try { var fileName = request.RequiredFileNameOrPath("image"); Guard.AgainstNull(_bookSelection.CurrentSelection, "CurrentBook"); var plainfilename = fileName.NotEncoded; // The fileName might be URL encoded. See https://silbloom.myjetbrains.com/youtrack/issue/BL-3901. var path = UrlPathString.GetFullyDecodedPath(_bookSelection.CurrentSelection.FolderPath, ref plainfilename); RequireThat.File(path).Exists(); var fileInfo = new FileInfo(path); dynamic result = new ExpandoObject(); result.name = plainfilename; result.bytes = fileInfo.Length; // Using a stream this way, according to one source, // http://stackoverflow.com/questions/552467/how-do-i-reliably-get-an-image-dimensions-in-net-without-loading-the-image, // supposedly avoids loading the image into memory when we only want its dimensions using (var stream = RobustFile.OpenRead(path)) using (var img = Image.FromStream(stream, false, false)) { result.width = img.Width; result.height = img.Height; switch (img.PixelFormat) { case PixelFormat.Format32bppArgb: case PixelFormat.Format32bppRgb: case PixelFormat.Format32bppPArgb: result.bitDepth = "32"; break; case PixelFormat.Format24bppRgb: result.bitDepth = "24"; break; case PixelFormat.Format16bppArgb1555: case PixelFormat.Format16bppGrayScale: result.bitDepth = "16"; break; case PixelFormat.Format8bppIndexed: result.bitDepth = "8"; break; case PixelFormat.Format1bppIndexed: result.bitDepth = "1"; break; default: result.bitDepth = "unknown"; break; } } request.ReplyWithJson((object)result); } catch (Exception e) { Logger.WriteEvent("Error in server imageInfo/: url was " + request.LocalPath()); Logger.WriteEvent("Error in server imageInfo/: exception is " + e.Message); request.Failed(e.Message); NonFatalProblem.Report(ModalIf.None, PassiveIf.Alpha, "Request Error", request.LocalPath(), e); } }
/// <summary> /// Adds a directory, along with all files and subdirectories, to the ZipStream. /// </summary> /// <param name="directoryToCompress">The directory to add recursively</param> /// <param name="zipStream">The ZipStream to which the files and directories will be added</param> /// <param name="dirNameOffset">This number of characters will be removed from the full directory or file name /// before creating the zip entry name</param> /// <param name="dirNamePrefix">string to prefix to the zip entry name</param> /// <param name="forReaderTools">If True, then some pre-processing will be done to the contents of decodable /// and leveled readers before they are added to the ZipStream</param> /// <param name="excludeAudio">If true, the contents of the audio directory will not be included</param> /// <param name="reduceImages">If true, images will be compressed before being added to the zip file</param> /// <param name="omitMetaJson">If true, meta.json is excluded (typically for HTML readers).</param> /// <para> name="reduceImages">If true, image files are reduced in size to no larger than 300x300 before saving</para> /// <remarks>Protected for testing purposes</remarks> private static void CompressDirectory(string directoryToCompress, ZipOutputStream zipStream, int dirNameOffset, string dirNamePrefix, bool forReaderTools, bool excludeAudio, bool reduceImages, bool omitMetaJson = false, string pathToFileForSha = null) { if (excludeAudio && Path.GetFileName(directoryToCompress).ToLowerInvariant() == "audio") { return; } var files = Directory.GetFiles(directoryToCompress); var bookFile = BookStorage.FindBookHtmlInFolder(directoryToCompress); foreach (var filePath in files) { if (ExcludedFileExtensionsLowerCase.Contains(Path.GetExtension(filePath.ToLowerInvariant()))) { continue; // BL-2246: skip putting this one into the BloomPack } var fileName = Path.GetFileName(filePath).ToLowerInvariant(); if (fileName.StartsWith(BookStorage.PrefixForCorruptHtmFiles)) { continue; } // Various stuff we keep in the book folder that is useful for editing or bloom library // or displaying collections but not needed by the reader. The most important is probably // eliminating the pdf, which can be very large. Note that we do NOT eliminate the // basic thumbnail.png, as we want eventually to extract that to use in the Reader UI. if (fileName == "thumbnail-70.png" || fileName == "thumbnail-256.png") { continue; } if (fileName == "meta.json" && omitMetaJson) { continue; } FileInfo fi = new FileInfo(filePath); var entryName = dirNamePrefix + filePath.Substring(dirNameOffset); // Makes the name in zip based on the folder entryName = ZipEntry.CleanName(entryName); // Removes drive from name and fixes slash direction ZipEntry newEntry = new ZipEntry(entryName) { DateTime = fi.LastWriteTime, IsUnicodeText = true }; // encode filename and comment in UTF8 byte[] modifiedContent = {}; // if this is a ReaderTools book, call GetBookReplacedWithTemplate() to get the contents if (forReaderTools && (bookFile == filePath)) { modifiedContent = Encoding.UTF8.GetBytes(GetBookReplacedWithTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else if (forReaderTools && (Path.GetFileName(filePath) == "meta.json")) { modifiedContent = Encoding.UTF8.GetBytes(GetMetaJsonModfiedForTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else if (reduceImages && ImageFileExtensions.Contains(Path.GetExtension(filePath.ToLowerInvariant()))) { modifiedContent = GetBytesOfReducedImage(filePath); newEntry.Size = modifiedContent.Length; } else if (reduceImages && (bookFile == filePath)) { var originalContent = File.ReadAllText(bookFile, Encoding.UTF8); var dom = XmlHtmlConverter.GetXmlDomFromHtml(originalContent); StripImagesWithMissingSrc(dom, bookFile); StripContentEditable(dom); InsertReaderStylesheet(dom); ConvertImagesToBackground(dom); var newContent = XmlHtmlConverter.ConvertDomToHtml5(dom); modifiedContent = Encoding.UTF8.GetBytes(newContent); newEntry.Size = modifiedContent.Length; if (pathToFileForSha != null) { // Make an extra entry containing the sha var sha = Book.MakeVersionCode(File.ReadAllText(pathToFileForSha, Encoding.UTF8), pathToFileForSha); var name = "version.txt"; // must match what BloomReader is looking for in NewBookListenerService.IsBookUpToDate() MakeExtraEntry(zipStream, name, sha); } MakeExtraEntry(zipStream, "readerStyles.css", File.ReadAllText(FileLocator.GetFileDistributedWithApplication(Path.Combine(BloomFileLocator.BrowserRoot, "publish", "android", "readerStyles.css")))); } else { newEntry.Size = fi.Length; } zipStream.PutNextEntry(newEntry); if (modifiedContent.Length > 0) { using (var memStream = new MemoryStream(modifiedContent)) { // There is some minimum buffer size (44 was too small); I don't know exactly what it is, // but 1024 makes it happy. StreamUtils.Copy(memStream, zipStream, new byte[Math.Max(modifiedContent.Length, 1024)]); } } else { // Zip the file in buffered chunks byte[] buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(filePath)) { StreamUtils.Copy(streamReader, zipStream, buffer); } } zipStream.CloseEntry(); } var folders = Directory.GetDirectories(directoryToCompress); foreach (var folder in folders) { var dirName = Path.GetFileName(folder); if ((dirName == null) || (dirName.ToLowerInvariant() == "sample texts")) { continue; // Don't want to bundle these up } CompressDirectory(folder, zipStream, dirNameOffset, dirNamePrefix, forReaderTools, excludeAudio, reduceImages); } }
/// <summary> /// Get a json of stats about the image. It is used to populate a tooltip when you hover over an image container /// </summary> private void HandleImageInfo(ApiRequest request) { try { var fileName = request.RequiredParam("image"); Guard.AgainstNull(_bookSelection.CurrentSelection, "CurrentBook"); var path = Path.Combine(_bookSelection.CurrentSelection.FolderPath, fileName); while (!RobustFile.Exists(path) && fileName.Contains('%')) { var fileName1 = fileName; // We can be fed doubly-encoded filenames. So try to decode a second time and see if that works. // See https://silbloom.myjetbrains.com/youtrack/issue/BL-3749. // Effectively triple-encoded filenames have cropped up for particular books. Such files are // already handled okay by EnhancedImageServer.ProcessAnyFileContent(). This code can handle // any depth of url-encoding. // See https://silbloom.myjetbrains.com/youtrack/issue/BL-5757. fileName = System.Web.HttpUtility.UrlDecode(fileName); if (fileName == fileName1) { break; } path = Path.Combine(_bookSelection.CurrentSelection.FolderPath, fileName); } RequireThat.File(path).Exists(); var fileInfo = new FileInfo(path); dynamic result = new ExpandoObject(); result.name = fileName; result.bytes = fileInfo.Length; // Using a stream this way, according to one source, // http://stackoverflow.com/questions/552467/how-do-i-reliably-get-an-image-dimensions-in-net-without-loading-the-image, // supposedly avoids loading the image into memory when we only want its dimensions using (var stream = RobustFile.OpenRead(path)) using (var img = Image.FromStream(stream, false, false)) { result.width = img.Width; result.height = img.Height; switch (img.PixelFormat) { case PixelFormat.Format32bppArgb: case PixelFormat.Format32bppRgb: case PixelFormat.Format32bppPArgb: result.bitDepth = "32"; break; case PixelFormat.Format24bppRgb: result.bitDepth = "24"; break; case PixelFormat.Format16bppArgb1555: case PixelFormat.Format16bppGrayScale: result.bitDepth = "16"; break; case PixelFormat.Format8bppIndexed: result.bitDepth = "8"; break; case PixelFormat.Format1bppIndexed: result.bitDepth = "1"; break; default: result.bitDepth = "unknown"; break; } } request.ReplyWithJson((object)result); } catch (Exception e) { Logger.WriteError("Error in server imageInfo/: url was " + request.LocalPath(), e); request.Failed(e.Message); } }
/// <summary> /// Adds a directory, along with all files and subdirectories, to the ZipStream. /// </summary> /// <param name="directoryToCompress">The directory to add recursively</param> /// <param name="zipStream">The ZipStream to which the files and directories will be added</param> /// <param name="dirNameOffset">This number of characters will be removed from the full directory or file name /// before creating the zip entry name</param> /// <param name="dirNamePrefix">string to prefix to the zip entry name</param> /// <param name="depthFromCollection">int with the number of folders away it is from the collection folder. The collection folder itself is 0, /// a book is 1, a subfolder of the book is 2, etc.</param> /// <param name="forReaderTools">If True, then some pre-processing will be done to the contents of decodable /// and leveled readers before they are added to the ZipStream</param> /// <param name="excludeAudio">If true, the contents of the audio directory will not be included</param> /// <param name="reduceImages">If true, image files are reduced in size to no larger than the max size before saving</para> /// <param name="omitMetaJson">If true, meta.json is excluded (typically for HTML readers).</param> private static void CompressDirectory(string directoryToCompress, ZipOutputStream zipStream, int dirNameOffset, string dirNamePrefix, int depthFromCollection, bool forReaderTools, bool excludeAudio, bool reduceImages, bool omitMetaJson = false, string pathToFileForSha = null) { if (excludeAudio && Path.GetFileName(directoryToCompress).ToLowerInvariant() == "audio") { return; } var files = Directory.GetFiles(directoryToCompress); // Don't get distracted by HTML files in any folder other than the book folder. // These HTML files in other locations aren't generated by Bloom. They may not have the format Bloom expects, // causing needless parsing errors to be thrown if we attempt to read them using Bloom code. bool shouldScanHtml = depthFromCollection == 1; // 1 means 1 level below the collection level, i.e. this is the book level var bookFile = shouldScanHtml ? BookStorage.FindBookHtmlInFolder(directoryToCompress) : null; XmlDocument dom = null; List <string> imagesToGiveTransparentBackgrounds = null; List <string> imagesToPreserveResolution = null; // Tests can also result in bookFile being null. if (!String.IsNullOrEmpty(bookFile)) { var originalContent = File.ReadAllText(bookFile, Encoding.UTF8); dom = XmlHtmlConverter.GetXmlDomFromHtml(originalContent); var fullScreenAttr = dom.GetElementsByTagName("body").Cast <XmlElement>().First().Attributes["data-bffullscreenpicture"]?.Value; if (fullScreenAttr != null && fullScreenAttr.IndexOf("bloomReader", StringComparison.InvariantCulture) >= 0) { // This feature (currently used for motion books in landscape mode) triggers an all-black background, // due to a rule in bookFeatures.less. // Making white pixels transparent on an all-black background makes line-art disappear, // which is bad (BL-6564), so just make an empty list in this case. imagesToGiveTransparentBackgrounds = new List <string>(); } else { imagesToGiveTransparentBackgrounds = FindCoverImages(dom); } imagesToPreserveResolution = FindImagesToPreserveResolution(dom); FindBackgroundAudioFiles(dom); } else { imagesToGiveTransparentBackgrounds = new List <string>(); imagesToPreserveResolution = new List <string>(); } // Some of the knowledge about ExcludedFileExtensions might one day move into this method. // But we'd have to check carefully the other places it is used. var localOnlyFiles = BookStorage.LocalOnlyFiles(directoryToCompress); foreach (var filePath in files) { if (ExcludedFileExtensionsLowerCase.Contains(Path.GetExtension(filePath.ToLowerInvariant()))) { continue; // BL-2246: skip putting this one into the BloomPack } if (IsUnneededWaveFile(filePath, depthFromCollection)) { continue; } if (localOnlyFiles.Contains(filePath)) { continue; } var fileName = Path.GetFileName(filePath).ToLowerInvariant(); if (fileName.StartsWith(BookStorage.PrefixForCorruptHtmFiles)) { continue; } // Various stuff we keep in the book folder that is useful for editing or bloom library // or displaying collections but not needed by the reader. The most important is probably // eliminating the pdf, which can be very large. Note that we do NOT eliminate the // basic thumbnail.png, as we want eventually to extract that to use in the Reader UI. if (fileName == "thumbnail-70.png" || fileName == "thumbnail-256.png") { continue; } if (fileName == "meta.json" && omitMetaJson) { continue; } FileInfo fi = new FileInfo(filePath); var entryName = dirNamePrefix + filePath.Substring(dirNameOffset); // Makes the name in zip based on the folder entryName = ZipEntry.CleanName(entryName); // Removes drive from name and fixes slash direction ZipEntry newEntry = new ZipEntry(entryName) { DateTime = fi.LastWriteTime, IsUnicodeText = true }; // encode filename and comment in UTF8 byte[] modifiedContent = {}; // if this is a ReaderTools book, call GetBookReplacedWithTemplate() to get the contents if (forReaderTools && (bookFile == filePath)) { modifiedContent = Encoding.UTF8.GetBytes(GetBookReplacedWithTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else if (forReaderTools && (Path.GetFileName(filePath) == "meta.json")) { modifiedContent = Encoding.UTF8.GetBytes(GetMetaJsonModfiedForTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else if (reduceImages && ImageFileExtensions.Contains(Path.GetExtension(filePath.ToLowerInvariant()))) { fileName = Path.GetFileName(filePath); // restore original capitalization if (imagesToPreserveResolution.Contains(fileName)) { modifiedContent = RobustFile.ReadAllBytes(filePath); } else { // Cover images should be transparent if possible. Others don't need to be. var makeBackgroundTransparent = imagesToGiveTransparentBackgrounds.Contains(fileName); modifiedContent = GetImageBytesForElectronicPub(filePath, makeBackgroundTransparent); } newEntry.Size = modifiedContent.Length; } else if (Path.GetExtension(filePath).ToLowerInvariant() == ".bloomcollection") { modifiedContent = Encoding.UTF8.GetBytes(GetBloomCollectionModifiedForTemplate(filePath)); newEntry.Size = modifiedContent.Length; } // CompressBookForDevice is always called with reduceImages set. else if (reduceImages && bookFile == filePath) { SignLanguageApi.ProcessVideos(HtmlDom.SelectChildVideoElements(dom.DocumentElement).Cast <XmlElement>(), directoryToCompress); var newContent = XmlHtmlConverter.ConvertDomToHtml5(dom); modifiedContent = Encoding.UTF8.GetBytes(newContent); newEntry.Size = modifiedContent.Length; if (pathToFileForSha != null) { // Make an extra entry containing the sha var sha = Book.ComputeHashForAllBookRelatedFiles(pathToFileForSha); var name = "version.txt"; // must match what BloomReader is looking for in NewBookListenerService.IsBookUpToDate() MakeExtraEntry(zipStream, name, sha); LastVersionCode = sha; } } else { newEntry.Size = fi.Length; } zipStream.PutNextEntry(newEntry); if (modifiedContent.Length > 0) { using (var memStream = new MemoryStream(modifiedContent)) { // There is some minimum buffer size (44 was too small); I don't know exactly what it is, // but 1024 makes it happy. StreamUtils.Copy(memStream, zipStream, new byte[Math.Max(modifiedContent.Length, 1024)]); } } else { // Zip the file in buffered chunks byte[] buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(filePath)) { StreamUtils.Copy(streamReader, zipStream, buffer); } } zipStream.CloseEntry(); } var folders = Directory.GetDirectories(directoryToCompress); foreach (var folder in folders) { var dirName = Path.GetFileName(folder); if ((dirName == null) || (dirName.ToLowerInvariant() == "sample texts")) { continue; // Don't want to bundle these up } CompressDirectory(folder, zipStream, dirNameOffset, dirNamePrefix, depthFromCollection + 1, forReaderTools, excludeAudio, reduceImages); } }
/// <summary> /// Adds a directory, along with all files and subdirectories, to the ZipStream. /// </summary> /// <param name="directoryPath">The directory to add recursively</param> /// <param name="zipStream">The ZipStream to which the files and directories will be added</param> /// <param name="dirNameOffset">This number of characters will be removed from the full directory or file name /// before creating the zip entry name</param> /// <param name="dirNamePrefix">string to prefix to the zip entry name</param> /// <param name="forReaderTools">If True, then some pre-processing will be done to the contents of decodable /// and leveled readers before they are added to the ZipStream</param> /// <remarks>Protected for testing purposes</remarks> protected static void CompressDirectory(string directoryPath, ZipOutputStream zipStream, int dirNameOffset, string dirNamePrefix, bool forReaderTools) { if (Path.GetFileName(directoryPath).ToLowerInvariant() == "audio") { return; // don't want audio in bloompack. todo: test } var files = Directory.GetFiles(directoryPath); var bookFile = BookStorage.FindBookHtmlInFolder(directoryPath); foreach (var filePath in files) { if (_excludedFileExtensionsLowerCase.Contains(Path.GetExtension(filePath.ToLowerInvariant()))) { continue; // BL-2246: skip putting this one into the BloomPack } if (Path.GetFileName(filePath).StartsWith(BookStorage.PrefixForCorruptHtmFiles)) { continue; } FileInfo fi = new FileInfo(filePath); var entryName = dirNamePrefix + filePath.Substring(dirNameOffset); // Makes the name in zip based on the folder entryName = ZipEntry.CleanName(entryName); // Removes drive from name and fixes slash direction ZipEntry newEntry = new ZipEntry(entryName) { DateTime = fi.LastWriteTime }; newEntry.IsUnicodeText = true; // encode filename and comment in UTF8 byte[] modifiedContent = {}; // if this is a ReaderTools book, call GetBookReplacedWithTemplate() to get the contents if (forReaderTools && (bookFile == filePath)) { modifiedContent = Encoding.UTF8.GetBytes(GetBookReplacedWithTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else if (forReaderTools && (Path.GetFileName(filePath) == "meta.json")) { modifiedContent = Encoding.UTF8.GetBytes(GetMetaJsonModfiedForTemplate(filePath)); newEntry.Size = modifiedContent.Length; } else { newEntry.Size = fi.Length; } zipStream.PutNextEntry(newEntry); if (modifiedContent.Length > 0) { using (var memStream = new MemoryStream(modifiedContent)) { StreamUtils.Copy(memStream, zipStream, new byte[modifiedContent.Length]); } } else { // Zip the file in buffered chunks byte[] buffer = new byte[4096]; using (var streamReader = RobustFile.OpenRead(filePath)) { StreamUtils.Copy(streamReader, zipStream, buffer); } } zipStream.CloseEntry(); } var folders = Directory.GetDirectories(directoryPath); foreach (var folder in folders) { var dirName = Path.GetFileName(folder); if ((dirName == null) || (dirName.ToLowerInvariant() == "sample texts")) { continue; // Don't want to bundle these up } CompressDirectory(folder, zipStream, dirNameOffset, dirNamePrefix, forReaderTools); } }