/*
* GNU AFFERO GENERAL PUBLIC LICENSE
* Version 3, 19 November 2007
* Copyright (C) 2007 Free Software Foundation, Inc.
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
namespace UVtools.Core.Extensions;
public static class ZipArchiveExtensions
{
///
/// Used to specify what our overwrite policy
/// is for files we are extracting.
///
public enum Overwrite
{
Always,
IfNewer,
Never
}
///
/// Used to identify what we will do if we are
/// trying to create a zip file and it already
/// exists.
///
public enum ArchiveAction
{
Merge,
Replace,
Error,
Ignore
}
///
/// Unzips the specified file to the given folder in a safe
/// manner. This plans for missing paths and existing files
/// and handles them gracefully.
///
///
/// The name of the zip file to be extracted
///
///
/// The directory to extract the zip file to
///
///
/// Specifies how we are going to handle an existing file.
/// The default is IfNewer.
///
public static void ImprovedExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer)
{
//Opens the zip file up to be read
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
archive.ImprovedExtractToDirectory(destinationDirectoryName, overwriteMethod);
}
///
/// Unzips the specified file to the given folder in a safe
/// manner. This plans for missing paths and existing files
/// and handles them gracefully.
///
///
/// The zip file to be extracted
///
///
/// The directory to extract the zip file to
///
///
/// Specifies how we are going to handle an existing file.
/// The default is IfNewer.
///
/// The number of extracted files
public static int ImprovedExtractToDirectory(this ZipArchive archive, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer)
{
int count = 0;
//Loops through each file in the zip file
foreach (var file in archive.Entries)
{
if(!string.IsNullOrEmpty(file.ImprovedExtractToFile(destinationDirectoryName, true, overwriteMethod))) count++;
}
return count;
}
///
/// Safely extracts a single file from a zip file
///
///
/// The zip entry we are pulling the file from
///
///
/// The root of where the file is going
///
/// True to preserve full name and create all directories up to the file, otherwise false to extract the file just to
///
/// Specifies how we are going to handle an existing file.
/// The default is Overwrite.IfNewer.
///
/// The extracted file path
public static string? ImprovedExtractToFile(this ZipArchiveEntry entry, string destinationPath, bool preserveFullName = true, Overwrite overwriteMethod = Overwrite.IfNewer)
{
//Gets the complete path for the destination file, including any
//relative paths that were in the zip file
var destFileName = Path.GetFullPath(Path.Combine(destinationPath, preserveFullName ? entry.FullName : entry.Name));
var fullDestDirPath = Path.GetFullPath(Path.Combine(destinationPath, (preserveFullName ? Path.GetDirectoryName(entry.FullName) : string.Empty)!) + Path.DirectorySeparatorChar);
if (!destFileName.StartsWith(fullDestDirPath)) return null; // Entry is outside the target dir
//Creates the directory (if it doesn't exist) for the new path
Directory.CreateDirectory(fullDestDirPath);
//Determines what to do with the file based upon the
//method of overwriting chosen
switch (overwriteMethod)
{
case Overwrite.Always:
//Just put the file in and overwrite anything that is found
entry.ExtractToFile(destFileName, true);
break;
case Overwrite.IfNewer:
//Checks to see if the file exists, and if so, if it should
//be overwritten
if (!File.Exists(destFileName) || File.GetLastWriteTime(destFileName) < entry.LastWriteTime)
{
//Either the file didn't exist or this file is newer, so
//we will extract it and overwrite any existing file
entry.ExtractToFile(destFileName, true);
}
break;
case Overwrite.Never:
//Put the file in if it is new but ignores the
//file if it already exists
if (!File.Exists(destFileName))
{
entry.ExtractToFile(destFileName);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(overwriteMethod), overwriteMethod, null);
}
return destFileName;
}
///
/// Allows you to add files to an archive, whether the archive
/// already exists or not
///
///
/// The name of the archive to you want to add your files to
///
///
/// A set of file names that are to be added
///
///
/// Specifies how we are going to handle an existing archive
///
///
///
/// Specifies what type of compression to use - defaults to Optimal
///
public static void AddToArchive(string archiveFullName,
List files,
ArchiveAction action = ArchiveAction.Replace,
Overwrite fileOverwrite = Overwrite.IfNewer,
CompressionLevel compression = CompressionLevel.Optimal)
{
//Identifies the mode we will be using - the default is Create
ZipArchiveMode mode = ZipArchiveMode.Create;
//Determines if the zip file even exists
bool archiveExists = File.Exists(archiveFullName);
//Figures out what to do based upon our specified overwrite method
switch (action)
{
case ArchiveAction.Merge:
//Sets the mode to update if the file exists, otherwise
//the default of Create is fine
if (archiveExists)
{
mode = ZipArchiveMode.Update;
}
break;
case ArchiveAction.Replace:
//Deletes the file if it exists. Either way, the default
//mode of Create is fine
if (archiveExists)
{
File.Delete(archiveFullName);
}
break;
case ArchiveAction.Error:
//Throws an error if the file exists
if (archiveExists)
{
throw new IOException($"The zip file {archiveFullName} already exists.");
}
break;
case ArchiveAction.Ignore:
//Closes the method silently and does nothing
if (archiveExists)
{
return;
}
break;
}
//Opens the zip file in the mode we specified
using ZipArchive zipFile = ZipFile.Open(archiveFullName, mode);
//This is a bit of a hack and should be refactored - I am
//doing a similar foreach loop for both modes, but for Create
//I am doing very little work while Update gets a lot of
//code. This also does not handle any other mode (of
//which there currently wouldn't be one since we don't
//use Read here).
if (mode == ZipArchiveMode.Create)
{
foreach (string file in files)
{
//Adds the file to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
}
}
else
{
foreach (string file in files)
{
var fileInZip = (from f in zipFile.Entries
where f.Name == Path.GetFileName(file)
select f).FirstOrDefault();
switch (fileOverwrite)
{
case Overwrite.Always:
//Deletes the file if it is found
if (fileInZip != null)
{
fileInZip.Delete();
}
//Adds the file to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
break;
case Overwrite.IfNewer:
//This is a bit trickier - we only delete the file if it is
//newer, but if it is newer or if the file isn't already in
//the zip file, we will write it to the zip file
if (fileInZip != null)
{
//Deletes the file only if it is older than our file.
//Note that the file will be ignored if the existing file
//in the archive is newer.
if (fileInZip.LastWriteTime < File.GetLastWriteTime(file))
{
fileInZip.Delete();
//Adds the file to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
}
}
else
{
//The file wasn't already in the zip file so add it to the archive
zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression);
}
break;
case Overwrite.Never:
//Don't do anything - this is a decision that you need to
//consider, however, since this will mean that no file will
//be written. You could write a second copy to the zip with
//the same name (not sure that is wise, however).
break;
}
}
}
}
///
/// Get or put a file into archive
///
///
/// Filename to create
/// Created
public static ZipArchiveEntry GetPutFile(this ZipArchive input, string filename)
{
return input.GetEntry(filename) ?? input.CreateEntry(filename);
}
///
/// Create or update a file into archive and write content to it
///
///
/// Filename to create
/// Content to write
///
/// Created
public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, string? content, ZipArchiveMode mode)
{
ZipArchiveEntry entry;
if (mode == ZipArchiveMode.Update)
{
entry = input.GetEntry(filename) ?? input.CreateEntry(filename);
}
else
{
entry = input.CreateEntry(filename);
}
if (string.IsNullOrEmpty(content)) return entry;
using var stream = entry.Open();
if (mode == ZipArchiveMode.Update) stream.SetLength(0);
using TextWriter tw = new StreamWriter(stream);
tw.Write(content);
return entry;
}
///
/// Create or update a file into archive and write content to it
///
///
/// Filename to create
/// Content to write
///
/// Created
public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, byte[]? content, ZipArchiveMode mode)
{
ZipArchiveEntry entry;
if (mode == ZipArchiveMode.Update)
{
entry = input.GetEntry(filename) ?? input.CreateEntry(filename);
}
else
{
entry = input.CreateEntry(filename);
}
if (content is null) return entry;
using var stream = entry.Open();
if (mode == ZipArchiveMode.Update) stream.SetLength(0);
stream.Write(content, 0, content.Length);
return entry;
}
///
/// Create or update a file into archive and write content to it
///
///
/// Filename to create
/// Content to write
///
/// Created
public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, Stream? content, ZipArchiveMode mode)
{
ZipArchiveEntry entry;
if (mode == ZipArchiveMode.Update)
{
entry = input.GetEntry(filename) ?? input.CreateEntry(filename);
}
else
{
entry = input.CreateEntry(filename);
}
if (content is null) return entry;
using var stream = entry.Open();
if (mode == ZipArchiveMode.Update) stream.SetLength(0);
content.CopyTo(stream);
return entry;
}
}