Skip to content

Commit f161f55

Browse files
Feature: Archive From ContextMenu
1 parent 8a8c697 commit f161f55

File tree

7 files changed

+287
-161
lines changed

7 files changed

+287
-161
lines changed

src/Files.App/Helpers/ContextFlyoutItemHelper.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.UI.Xaml.Input;
2020
using Microsoft.UI.Xaml.Media.Imaging;
2121
using System.Threading;
22+
using System.Configuration;
2223

2324
namespace Files.App.Helpers
2425
{
@@ -944,6 +945,22 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(BaseLayo
944945
ShowInZipPage = true,
945946
},
946947
new ContextMenuFlyoutItemViewModel()
948+
{
949+
Command = commandsViewModel.CompressSingleFolderCommand,
950+
Glyph = "\uE8DE",
951+
Text = string.Format("AddSingleItemToArchive/Text".GetLocalizedResource(), selectedItems.First().ItemName),
952+
ShowInSearchPage = true,
953+
ShowItem = selectedItems.Count == 1 && !selectedItems.First().IsZipItem,
954+
},
955+
new ContextMenuFlyoutItemViewModel()
956+
{
957+
Command = commandsViewModel.CompressMultipleFoldersCommand,
958+
Glyph = "\uE8DE",
959+
Text = "AddToArchive/Text".GetLocalizedResource(),
960+
ShowInSearchPage = true,
961+
ShowItem = selectedItems.Count > 1 && !selectedItems.First().IsZipItem,
962+
},
963+
new ContextMenuFlyoutItemViewModel()
947964
{
948965
Text = "BaseLayoutItemContextFlyoutExtractionOptions".GetLocalizedResource(),
949966
Glyph = "\xF11A",

src/Files.App/Helpers/ZipHelpers.cs

Lines changed: 174 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -6,125 +6,181 @@
66
using System.Linq;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using System.Windows.Shell;
910

1011
namespace Files.App.Helpers
1112
{
12-
public static class ZipHelpers
13-
{
14-
public static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder, IProgress<float> progressDelegate, CancellationToken cancellationToken)
15-
{
16-
using (SevenZipExtractor zipFile = await Filesystem.FilesystemTasks.Wrap(async () =>
17-
{
18-
var arch = new SevenZipExtractor(await archive.OpenStreamForReadAsync());
19-
return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u)
20-
}))
21-
{
22-
if (zipFile == null)
23-
{
24-
return;
25-
}
26-
//zipFile.IsStreamOwner = true;
27-
List<ArchiveFileInfo> directoryEntries = new List<ArchiveFileInfo>();
28-
List<ArchiveFileInfo> fileEntries = new List<ArchiveFileInfo>();
29-
foreach (ArchiveFileInfo entry in zipFile.ArchiveFileData)
30-
{
31-
if (!entry.IsDirectory)
32-
{
33-
fileEntries.Add(entry);
34-
}
35-
else
36-
{
37-
directoryEntries.Add(entry);
38-
}
39-
}
40-
41-
if (cancellationToken.IsCancellationRequested) // Check if cancelled
42-
{
43-
return;
44-
}
45-
46-
var directories = new List<string>();
47-
try
48-
{
49-
directories.AddRange(directoryEntries.Select((entry) => entry.FileName));
50-
directories.AddRange(fileEntries.Select((entry) => Path.GetDirectoryName(entry.FileName)));
51-
}
52-
catch (Exception ex)
53-
{
54-
App.Logger.Warn(ex, $"Error transforming zip names into: {destinationFolder.Path}\n" +
55-
$"Directories: {string.Join(", ", directoryEntries.Select(x => x.FileName))}\n" +
56-
$"Files: {string.Join(", ", fileEntries.Select(x => x.FileName))}");
57-
return;
58-
}
59-
60-
foreach (var dir in directories.Distinct().OrderBy(x => x.Length))
61-
{
62-
if (!NativeFileOperationsHelper.CreateDirectoryFromApp(dir, IntPtr.Zero))
63-
{
64-
var dirName = destinationFolder.Path;
65-
foreach (var component in dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries))
66-
{
67-
dirName = Path.Combine(dirName, component);
68-
NativeFileOperationsHelper.CreateDirectoryFromApp(dirName, IntPtr.Zero);
69-
}
70-
}
71-
72-
if (cancellationToken.IsCancellationRequested) // Check if canceled
73-
{
74-
return;
75-
}
76-
}
77-
78-
if (cancellationToken.IsCancellationRequested) // Check if canceled
79-
{
80-
return;
81-
}
82-
83-
// Fill files
84-
85-
byte[] buffer = new byte[4096];
86-
int entriesAmount = fileEntries.Count;
87-
int entriesFinished = 0;
88-
89-
foreach (var entry in fileEntries)
90-
{
91-
if (cancellationToken.IsCancellationRequested) // Check if canceled
92-
{
93-
return;
94-
}
95-
if (entry.Encrypted)
96-
{
97-
App.Logger.Info($"Skipped encrypted zip entry: {entry.FileName}");
98-
continue; // TODO: support password protected archives
99-
}
100-
101-
string filePath = Path.Combine(destinationFolder.Path, entry.FileName);
102-
103-
var hFile = NativeFileOperationsHelper.CreateFileForWrite(filePath);
104-
if (hFile.IsInvalid)
105-
{
106-
return; // TODO: handle error
107-
}
108-
109-
// We don't close hFile because FileStream.Dispose() already does that
110-
using (FileStream destinationStream = new FileStream(hFile, FileAccess.Write))
111-
{
112-
try
113-
{
114-
await zipFile.ExtractFileAsync(entry.Index, destinationStream);
115-
}
116-
catch (Exception ex)
117-
{
118-
App.Logger.Warn(ex, $"Error extracting file: {filePath}");
119-
return; // TODO: handle error
120-
}
121-
}
122-
123-
entriesFinished++;
124-
float percentage = (float)((float)entriesFinished / (float)entriesAmount) * 100.0f;
125-
progressDelegate?.Report(percentage);
126-
}
127-
}
128-
}
129-
}
13+
public static class ZipHelpers
14+
{
15+
public static async Task<bool> CompressSingleToArchive(string sourceFolder, string archive)
16+
{
17+
SevenZipCompressor compressor = new()
18+
{
19+
ArchiveFormat = OutArchiveFormat.Zip,
20+
CompressionLevel = CompressionLevel.High,
21+
EventSynchronization = EventSynchronizationStrategy.AlwaysAsynchronous,
22+
FastCompression = true,
23+
IncludeEmptyDirectories = true
24+
};
25+
26+
try
27+
{
28+
await compressor.CompressDirectoryAsync(sourceFolder, archive);
29+
return true;
30+
}
31+
catch (Exception ex)
32+
{
33+
App.Logger.Warn(ex, $"Error compressing folder: {sourceFolder}");
34+
}
35+
return false;
36+
}
37+
38+
public static async Task<bool> CompressMultipleToArchive(string[] sourceFolders, string archive, IProgress<float> progressDelegate)
39+
{
40+
SevenZipCompressor compressor = new()
41+
{
42+
ArchiveFormat = OutArchiveFormat.Zip,
43+
CompressionLevel = CompressionLevel.High,
44+
EventSynchronization = EventSynchronizationStrategy.AlwaysAsynchronous,
45+
FastCompression = true,
46+
IncludeEmptyDirectories = true,
47+
PreserveDirectoryRoot = true
48+
};
49+
50+
bool noErrors = true;
51+
try
52+
{
53+
for (int i = 0; i < sourceFolders.Length; i++)
54+
{
55+
if (i > 0)
56+
compressor.CompressionMode = CompressionMode.Append;
57+
58+
await compressor.CompressDirectoryAsync(sourceFolders[i], archive);
59+
float percentage = (i + 1.0f) / sourceFolders.Length * 100.0f;
60+
progressDelegate?.Report(percentage);
61+
}
62+
}
63+
catch (Exception ex)
64+
{
65+
App.Logger.Warn(ex, $"Error compressing folder: {archive}");
66+
noErrors = false;
67+
}
68+
return noErrors;
69+
}
70+
71+
public static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder, IProgress<float> progressDelegate, CancellationToken cancellationToken)
72+
{
73+
using (SevenZipExtractor zipFile = await Filesystem.FilesystemTasks.Wrap(async () =>
74+
{
75+
var arch = new SevenZipExtractor(await archive.OpenStreamForReadAsync());
76+
return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u)
77+
}))
78+
{
79+
if (zipFile == null)
80+
return;
81+
//zipFile.IsStreamOwner = true;
82+
List<ArchiveFileInfo> directoryEntries = new List<ArchiveFileInfo>();
83+
List<ArchiveFileInfo> fileEntries = new List<ArchiveFileInfo>();
84+
foreach (ArchiveFileInfo entry in zipFile.ArchiveFileData)
85+
{
86+
if (!entry.IsDirectory)
87+
fileEntries.Add(entry);
88+
else
89+
directoryEntries.Add(entry);
90+
}
91+
92+
if (cancellationToken.IsCancellationRequested) // Check if cancelled
93+
return;
94+
95+
var directories = new List<string>();
96+
try
97+
{
98+
directories.AddRange(directoryEntries.Select((entry) => entry.FileName));
99+
directories.AddRange(fileEntries.Select((entry) => Path.GetDirectoryName(entry.FileName)));
100+
}
101+
catch (Exception ex)
102+
{
103+
App.Logger.Warn(ex, $"Error transforming zip names into: {destinationFolder.Path}\n" +
104+
$"Directories: {string.Join(", ", directoryEntries.Select(x => x.FileName))}\n" +
105+
$"Files: {string.Join(", ", fileEntries.Select(x => x.FileName))}");
106+
return;
107+
}
108+
109+
foreach (var dir in directories.Distinct().OrderBy(x => x.Length))
110+
{
111+
if (!NativeFileOperationsHelper.CreateDirectoryFromApp(dir, IntPtr.Zero))
112+
{
113+
var dirName = destinationFolder.Path;
114+
foreach (var component in dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries))
115+
{
116+
dirName = Path.Combine(dirName, component);
117+
NativeFileOperationsHelper.CreateDirectoryFromApp(dirName, IntPtr.Zero);
118+
}
119+
}
120+
121+
if (cancellationToken.IsCancellationRequested) // Check if canceled
122+
return;
123+
}
124+
125+
if (cancellationToken.IsCancellationRequested) // Check if canceled
126+
return;
127+
128+
// Fill files
129+
130+
byte[] buffer = new byte[4096];
131+
int entriesAmount = fileEntries.Count;
132+
int entriesFinished = 0;
133+
134+
foreach (var entry in fileEntries)
135+
{
136+
if (cancellationToken.IsCancellationRequested) // Check if canceled
137+
return;
138+
if (entry.Encrypted)
139+
{
140+
App.Logger.Info($"Skipped encrypted zip entry: {entry.FileName}");
141+
continue; // TODO: support password protected archives
142+
}
143+
144+
string filePath = Path.Combine(destinationFolder.Path, entry.FileName);
145+
146+
var hFile = NativeFileOperationsHelper.CreateFileForWrite(filePath);
147+
if (hFile.IsInvalid)
148+
return; // TODO: handle error
149+
150+
// We don't close hFile because FileStream.Dispose() already does that
151+
using (FileStream destinationStream = new FileStream(hFile, FileAccess.Write))
152+
{
153+
try
154+
{
155+
await zipFile.ExtractFileAsync(entry.Index, destinationStream);
156+
}
157+
catch (Exception ex)
158+
{
159+
App.Logger.Warn(ex, $"Error extracting file: {filePath}");
160+
return; // TODO: handle error
161+
}
162+
}
163+
164+
entriesFinished++;
165+
float percentage = (float)((float)entriesFinished / (float)entriesAmount) * 100.0f;
166+
progressDelegate?.Report(percentage);
167+
}
168+
}
169+
}
170+
171+
private static async Task CopyDirectory(BaseStorageFolder source, BaseStorageFolder destination)
172+
{
173+
foreach (var file in await source.GetFilesAsync())
174+
{
175+
await file.CopyAsync(destination);
176+
}
177+
foreach (var folder in await destination.GetFoldersAsync())
178+
{
179+
string subDirectoryPath = Path.Combine(destination.Path, folder.Name);
180+
NativeFileOperationsHelper.CreateDirectoryFromApp(subDirectoryPath, IntPtr.Zero);
181+
BaseStorageFolder subDirectory = await StorageHelpers.ToStorageItem<BaseStorageFolder>(subDirectoryPath);
182+
await CopyDirectory(folder, subDirectory);
183+
}
184+
}
185+
}
130186
}

0 commit comments

Comments
 (0)