diff --git a/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs b/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs
index f7664d02af5b..f36d0dab9565 100644
--- a/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs
+++ b/src/Files.App/Actions/FileSystem/BaseDeleteAction.cs
@@ -16,6 +16,7 @@ public virtual ActionCategory Category
public override bool IsExecutable =>
context.HasSelection &&
+ (OperatingSystem.IsWindows() || context.PageType != ContentPageTypes.ZipFolder) &&
(!context.ShellPage?.SlimContentPage?.IsRenamingItem ?? false) &&
UIHelpers.CanShowDialog;
@@ -47,7 +48,7 @@ item.PrimaryItemAttribute is StorageItemTypes.File
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName is nameof(IContentPageContext.HasSelection))
+ if (e.PropertyName is nameof(IContentPageContext.HasSelection) or nameof(IContentPageContext.PageType))
OnPropertyChanged(nameof(IsExecutable));
}
}
diff --git a/src/Files.App/Actions/FileSystem/OpenArchiveAsFolderAction.cs b/src/Files.App/Actions/FileSystem/OpenArchiveAsFolderAction.cs
new file mode 100644
index 000000000000..6eafe9a967b0
--- /dev/null
+++ b/src/Files.App/Actions/FileSystem/OpenArchiveAsFolderAction.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+using Files.Shared.Helpers;
+using Windows.Storage;
+
+namespace Files.App.Actions
+{
+ ///
+ /// Browses an archive like a folder (Linux), while double-click keeps opening it in the default application.
+ ///
+ [GeneratedRichCommand]
+ internal sealed partial class OpenArchiveAsFolderAction : ObservableObject, IAction
+ {
+ private readonly IContentPageContext context;
+
+ public string Label
+ => Strings.OpenArchiveAsFolder.GetLocalizedResource();
+
+ public string Description
+ => Strings.OpenArchiveAsFolderDescription.GetLocalizedResource();
+
+ public ActionCategory Category
+ => ActionCategory.Open;
+
+ public RichGlyph Glyph
+ => new(themedIconStyle: "App.ThemedIcons.Folder");
+
+ public bool IsExecutable =>
+ !OperatingSystem.IsWindows() &&
+ context.ShellPage is not null &&
+ context.PageType is ContentPageTypes.Folder &&
+ context.SelectedItems.Count == 1 &&
+ context.SelectedItem is { PrimaryItemAttribute: StorageItemTypes.File, ItemPath: { } path } &&
+ FileExtensionHelpers.IsZipPath(path);
+
+ public OpenArchiveAsFolderAction()
+ {
+ context = Ioc.Default.GetRequiredService();
+
+ context.PropertyChanged += Context_PropertyChanged;
+ }
+
+ public Task ExecuteAsync(object? parameter = null)
+ {
+ if (context.ShellPage is not { } shellPage || context.SelectedItem?.ItemPath is not { } path || !IsExecutable)
+ return Task.CompletedTask;
+
+ shellPage.NavigateWithArguments(shellPage.InstanceViewModel.FolderSettings.GetLayoutType(path), new NavigationArguments()
+ {
+ NavPathParam = path,
+ AssociatedTabInstance = shellPage,
+ });
+
+ return Task.CompletedTask;
+ }
+
+ private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is nameof(IContentPageContext.SelectedItems) or nameof(IContentPageContext.PageType) or nameof(IContentPageContext.HasSelection))
+ OnPropertyChanged(nameof(IsExecutable));
+ }
+ }
+}
diff --git a/src/Files.App/Actions/FileSystem/PasteItemAction.cs b/src/Files.App/Actions/FileSystem/PasteItemAction.cs
index f7c5079e0713..3b5c0f275f63 100644
--- a/src/Files.App/Actions/FileSystem/PasteItemAction.cs
+++ b/src/Files.App/Actions/FileSystem/PasteItemAction.cs
@@ -54,6 +54,7 @@ public bool GetIsExecutable()
{
return
App.AppModel.IsPasteEnabled &&
+ (OperatingSystem.IsWindows() || context.PageType != ContentPageTypes.ZipFolder) &&
context.PageType != ContentPageTypes.Home &&
context.PageType != ContentPageTypes.RecycleBin &&
context.PageType != ContentPageTypes.SearchResults &&
diff --git a/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs b/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs
index a0c689340b32..df31dec919a8 100644
--- a/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs
+++ b/src/Files.App/Actions/FileSystem/PasteItemAsShortcutAction.cs
@@ -49,6 +49,7 @@ public bool GetIsExecutable()
return
Ioc.Default.GetRequiredService().SupportsShortcutFiles &&
App.AppModel.IsPasteEnabled &&
+ (OperatingSystem.IsWindows() || context.PageType != ContentPageTypes.ZipFolder) &&
context.PageType != ContentPageTypes.Home &&
context.PageType != ContentPageTypes.RecycleBin &&
context.PageType != ContentPageTypes.SearchResults &&
diff --git a/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs b/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs
index 7862e5e88399..5f4affcd474e 100644
--- a/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs
+++ b/src/Files.App/Actions/FileSystem/PasteItemToSelectionAction.cs
@@ -56,6 +56,9 @@ public bool GetIsExecutable()
if (!App.AppModel.IsPasteEnabled)
return false;
+ if (!OperatingSystem.IsWindows() && context.PageType is ContentPageTypes.ZipFolder)
+ return false;
+
if (context.PageType is ContentPageTypes.Home or ContentPageTypes.RecycleBin or ContentPageTypes.SearchResults or ContentPageTypes.ReleaseNotes or ContentPageTypes.Settings)
return false;
diff --git a/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs b/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs
index ebd1f4068c87..b8c20938d7d7 100644
--- a/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs
+++ b/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs
@@ -10,7 +10,7 @@ internal abstract class BaseTransferItemAction : ObservableObject
protected readonly IContentPageContext ContentPageContext = Ioc.Default.GetRequiredService();
protected readonly StatusCenterViewModel StatusCenterViewModel = Ioc.Default.GetRequiredService();
- public bool IsExecutable
+ public virtual bool IsExecutable
=> ContentPageContext.HasSelection;
public BaseTransferItemAction()
@@ -25,7 +25,7 @@ public async Task ExecuteTransferAsync(DataPackageOperation type = DataPackageOp
private void ContentPageContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
- if (e.PropertyName is nameof(IContentPageContext.HasSelection))
+ if (e.PropertyName is nameof(IContentPageContext.HasSelection) or nameof(IContentPageContext.PageType))
OnPropertyChanged(nameof(IsExecutable));
}
}
diff --git a/src/Files.App/Actions/FileSystem/Transfer/CutItemAction.cs b/src/Files.App/Actions/FileSystem/Transfer/CutItemAction.cs
index cf74ace23395..8b4807900c54 100644
--- a/src/Files.App/Actions/FileSystem/Transfer/CutItemAction.cs
+++ b/src/Files.App/Actions/FileSystem/Transfer/CutItemAction.cs
@@ -29,6 +29,10 @@ public string AccessKey
public HotKey HotKey
=> new(Keys.X, KeyModifiers.Ctrl);
+ // Archives are read-only, so their items can only be copied out
+ public override bool IsExecutable
+ => base.IsExecutable && (OperatingSystem.IsWindows() || ContentPageContext.PageType != ContentPageTypes.ZipFolder);
+
public CutItemAction() : base()
{
}
diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs
index 4ce50a65c762..905019b1b19e 100644
--- a/src/Files.App/App.xaml.cs
+++ b/src/Files.App/App.xaml.cs
@@ -80,6 +80,10 @@ public App()
};
AppDomain.CurrentDomain.ProcessExit += static (_, _) =>
SafetyExtensions.IgnoreExceptions(() => Ioc.Default.GetService()?.TryCompleteAndFlush(TimeSpan.FromSeconds(2)));
+#if !WINDOWS
+ AppDomain.CurrentDomain.ProcessExit += static (_, _) => SafetyExtensions.IgnoreExceptions(ArchiveOpenTempStore.CleanupCurrentProcess);
+ _ = Task.Run(ArchiveOpenTempStore.CleanupStale);
+#endif
}
#if WINDOWS
diff --git a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs
index 3a43995d9e99..65c87bd92d21 100644
--- a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs
+++ b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs
@@ -389,6 +389,7 @@ public static List GetBaseItemMenuItems(
IsVisible = currentInstanceViewModel.IsPageTypeRecycleBin && itemsSelected,
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(Commands.OpenItem).Build(),
+ new ContextMenuFlyoutItemViewModelBuilder(Commands.OpenArchiveAsFolder).Build(),
new ContextMenuFlyoutItemViewModelBuilder(Commands.OpenItemWithApplicationPicker)
{
Tag = "OpenWith",
diff --git a/src/Files.App/Helpers/Navigation/ArchiveOpenTempStore.Linux.cs b/src/Files.App/Helpers/Navigation/ArchiveOpenTempStore.Linux.cs
new file mode 100644
index 000000000000..6377257d2db4
--- /dev/null
+++ b/src/Files.App/Helpers/Navigation/ArchiveOpenTempStore.Linux.cs
@@ -0,0 +1,172 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+#if !WINDOWS
+using System.IO;
+using System.Text;
+
+namespace Files.App.Helpers
+{
+ ///
+ /// Private (0700) scratch space for archive members that are extracted so they can be opened.
+ /// Each process uses its own p<pid> directory; leftovers of dead processes are removed at start.
+ ///
+ internal static class ArchiveOpenTempStore
+ {
+ private static readonly object gate = new();
+ private static string? processDirectory;
+
+ private static string Root
+ {
+ get
+ {
+ var runtime = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
+ var baseDir = !string.IsNullOrEmpty(runtime) && Path.IsPathRooted(runtime) && Directory.Exists(runtime)
+ ? Path.Combine(runtime, "linuxfiles")
+ : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache", "linuxfiles");
+ return Path.Combine(baseDir, "archive-open");
+ }
+ }
+
+ private const UnixFileMode PrivateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute;
+
+ ///
+ /// Creates (or validates) a directory component: it must be a real directory, not a symlink, and closed to group and others.
+ /// A directory owned by someone else that passes this is still unusable, since it cannot be written to.
+ ///
+ private static bool EnsurePrivateDirectory(string path, bool create)
+ {
+ var info = new DirectoryInfo(path);
+ if (!info.Exists && !File.Exists(path) && info.LinkTarget is null)
+ {
+ if (!create)
+ return false;
+ Directory.CreateDirectory(path, PrivateMode);
+ info.Refresh();
+ }
+
+ return info.LinkTarget is null
+ && info.Exists
+ && (File.GetUnixFileMode(path) & (UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute)) == 0;
+ }
+
+ private static bool EnsureRoot(bool create)
+ {
+ var root = Root;
+ var parent = Path.GetDirectoryName(root)!;
+ if (create)
+ Directory.CreateDirectory(Path.GetDirectoryName(parent)!);
+ // The parent must be private too: an entry in a directory someone else can write to could be swapped for a symlink after it is checked
+ return EnsurePrivateDirectory(parent, create) && EnsurePrivateDirectory(root, create);
+ }
+
+ /// Creates a fresh private directory and returns the path for a sanitized file name inside it.
+ public static string CreateFilePath(string entryName)
+ {
+ lock (gate)
+ {
+ if (!EnsureRoot(create: true))
+ throw new IOException("The archive scratch directory is not private.");
+
+ processDirectory ??= Path.Combine(Root, "p" + Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ if (!EnsurePrivateDirectory(processDirectory, create: true))
+ throw new IOException("The archive scratch directory is not private.");
+
+ var directory = Path.Combine(processDirectory, Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory, PrivateMode);
+ return Path.Combine(directory, SanitizeName(entryName));
+ }
+ }
+
+ /// Removes the directory holding a file created by .
+ public static void Discard(string filePath)
+ => TryDelete(Path.GetDirectoryName(filePath));
+
+ /// Keeps only the last path segment, with control and separator characters replaced.
+ public static string SanitizeName(string entryPath)
+ {
+ var name = entryPath.Replace('\\', '/');
+ name = name[(name.LastIndexOf('/') + 1)..];
+ var builder = new StringBuilder(name.Length);
+ foreach (var c in name)
+ builder.Append(char.IsControl(c) || c == '/' || c == '\0' ? '_' : c);
+
+ var result = builder.ToString().Trim();
+ if (result.Length > 120)
+ {
+ // Keep the extension: it decides which application opens the file
+ var extension = Path.GetExtension(result);
+ if (extension.Length is 0 or > 40)
+ result = result[..120];
+ else
+ result = result[..(120 - extension.Length)] + extension;
+ }
+ return result is "" or "." or ".." ? "file" : result;
+ }
+
+ /// Removes this process's extracted files.
+ public static void CleanupCurrentProcess()
+ {
+ string? directory;
+ lock (gate)
+ directory = processDirectory;
+
+ if (directory is not null && EnsureRootSafe())
+ TryDelete(directory);
+ }
+
+ /// Removes directories left behind by processes that no longer exist.
+ public static void CleanupStale()
+ {
+ try
+ {
+ var root = Root;
+ if (!EnsureRootSafe())
+ return;
+
+ foreach (var directory in Directory.EnumerateDirectories(root, "p*"))
+ {
+ var name = Path.GetFileName(directory);
+ if (int.TryParse(name.AsSpan(1), out var pid) && pid != Environment.ProcessId && Directory.Exists("/proc/" + pid.ToString(System.Globalization.CultureInfo.InvariantCulture)))
+ continue;
+
+ if (pid == Environment.ProcessId)
+ continue;
+
+ TryDelete(directory);
+ }
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ }
+ }
+
+ private static bool EnsureRootSafe()
+ {
+ try
+ {
+ return EnsureRoot(create: false);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ return false;
+ }
+ }
+
+ private static void TryDelete(string? directory)
+ {
+ if (directory is null)
+ return;
+
+ try
+ {
+ if (Directory.Exists(directory))
+ Directory.Delete(directory, recursive: true);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ }
+ }
+ }
+}
+#endif
diff --git a/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs b/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
index 79372fb96256..3b121687f805 100644
--- a/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
+++ b/src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
@@ -45,9 +45,23 @@ private static async Task OpenPathLinuxAsync(string path, IShellPage assoc
return true;
}
- // LINUX-TODO(archives): open archive members by extracting them to a private temporary copy first
- await DialogDisplayHelper.ShowDialogAsync(Strings.LinuxOpenFailedTitle.GetLocalizedResource(), Strings.LinuxOpenFailedText.GetLocalizedFormatResource(DisplaySanitizer.Field(path)));
- return false;
+ if (resolved.Item is not OwlCore.Storage.IFile member)
+ {
+ await DialogDisplayHelper.ShowDialogAsync(Strings.LinuxOpenFailedTitle.GetLocalizedResource(), Strings.LinuxOpenFailedText.GetLocalizedFormatResource(DisplaySanitizer.Field(path)));
+ return false;
+ }
+
+ // Extract only this entry (size and ratio limits apply in the archive service) and open the private copy,
+ // so the usual confirmation gates see a real file
+ var temporaryPath = await ExtractArchiveMemberAsync(member);
+ if (temporaryPath is null)
+ return false;
+
+ var openedMember = await OpenFileLinuxAsync(temporaryPath, openViaApplicationPicker);
+ if (!openedMember)
+ await DialogDisplayHelper.ShowDialogAsync(Strings.LinuxOpenFailedTitle.GetLocalizedResource(), Strings.LinuxOpenFailedText.GetLocalizedFormatResource(DisplaySanitizer.Field(path)));
+
+ return openedMember;
}
if (!File.Exists(path))
@@ -64,13 +78,49 @@ private static async Task OpenPathLinuxAsync(string path, IShellPage assoc
return opened;
}
- catch (Exception ex) when (ex is not OperationCanceledException)
+ catch (OperationCanceledException)
+ {
+ return true;
+ }
+ catch (Exception ex)
{
App.Logger.LogWarning(ex, "Failed to open {Path}", path);
return false;
}
}
+ private static async Task ExtractArchiveMemberAsync(OwlCore.Storage.IFile member)
+ {
+ var name = member is Files.App.Storage.Archives.ArchiveEntryFile entry ? entry.Entry.Path : member.Name;
+ string? target = null;
+ try
+ {
+ target = ArchiveOpenTempStore.CreateFilePath(name);
+ await using var input = await member.OpenReadAsync();
+ await using var output = new FileStream(target, new FileStreamOptions
+ {
+ Mode = FileMode.CreateNew,
+ Access = FileAccess.Write,
+ UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite,
+ });
+ await input.CopyToAsync(output);
+ return target;
+ }
+ catch (OperationCanceledException)
+ {
+ // Password prompt cancelled by the user
+ if (target is not null)
+ ArchiveOpenTempStore.Discard(target);
+ return null;
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarning(ex, "Failed to extract archive member {Name}", DisplaySanitizer.Field(name));
+ await DialogDisplayHelper.ShowDialogAsync(Strings.LinuxOpenFailedTitle.GetLocalizedResource(), Strings.LinuxOpenFailedText.GetLocalizedFormatResource(DisplaySanitizer.Field(name)));
+ return null;
+ }
+ }
+
///
/// Opens several files. Files that need a gate (executables, launchers) are opened one by one through
/// ; plain documents are passed to their default applications together.
diff --git a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs
index d5bd2dc370be..334ee4f9d871 100644
--- a/src/Files.App/Helpers/Navigation/NavigationHelpers.cs
+++ b/src/Files.App/Helpers/Navigation/NavigationHelpers.cs
@@ -318,7 +318,7 @@ private static string[] AncestorHints(string? path)
}
else
{
- tabLocationHeader = currentPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar).Split('\\', StringSplitOptions.RemoveEmptyEntries).Last();
+ tabLocationHeader = currentPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar).Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries).Last();
var rootItem = await FilesystemTasks.WrapNullable(() => DriveHelpers.GetRootFromPathAsync(currentPath));
if (rootItem)
diff --git a/src/Files.App/Platforms/Desktop/Generated/ICommandManager.g.cs b/src/Files.App/Platforms/Desktop/Generated/ICommandManager.g.cs
index 3cda0be855af..a8b88efda0fc 100644
--- a/src/Files.App/Platforms/Desktop/Generated/ICommandManager.g.cs
+++ b/src/Files.App/Platforms/Desktop/Generated/ICommandManager.g.cs
@@ -111,6 +111,7 @@ public interface ICommandManager : IEnumerable
IRichCommand NewWindow { get; }
IRichCommand NextTab { get; }
IRichCommand OpenAllTagged { get; }
+ IRichCommand OpenArchiveAsFolder { get; }
IRichCommand OpenClassicProperties { get; }
IRichCommand OpenCommandPalette { get; }
IRichCommand OpenCurrentFolderInOtherPane { get; }
diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw
index df00781c8866..74f7fcc91985 100644
--- a/src/Files.App/Strings/en-US/Resources.resw
+++ b/src/Files.App/Strings/en-US/Resources.resw
@@ -4971,4 +4971,11 @@
LinuxFiles
Display name of the Linux port, shown in the window title and About page on Linux.
+
+ Open as folder
+ Context menu entry on Linux that browses an archive like a folder.
+
+
+ Browse the contents of the selected archive like a folder
+
diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
index 349e1deb66a4..9dbee7e42b18 100644
--- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
+++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
@@ -150,6 +150,7 @@ public async Task DragOverAsync(DragEventArgs? e)
// As long as one file doesn't already belong to this folder, and this folder isn't one of the dragged items or inside one
if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults ||
+ (!OperatingSystem.IsWindows() && ZipStorageFolder.IsZipPath(workingDirectoryPath)) ||
draggedItems.Any() && (draggedItems.AreItemsAlreadyInFolder(workingDirectoryPath) || draggedItems.ContainsDestinationOrAncestor(workingDirectoryPath)))
{
e.AcceptedOperation = DataPackageOperation.None;
diff --git a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
index eac3edf97ca3..59b028c4e503 100644
--- a/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs
@@ -480,7 +480,9 @@ public async Task PathBoxItem_DragOver(object sender, DragEventArgs e)
var storageItems = await FilesystemHelpers.GetDraggedStorageItems(e.DataView);
- if (storageItems.ContainsDestinationOrAncestor(pathBoxItem.Path) ||
+ // Archives are read-only: nothing can be dropped into them
+ if ((!OperatingSystem.IsWindows() && ZipStorageFolder.IsZipPath(pathBoxItem.Path)) ||
+ storageItems.ContainsDestinationOrAncestor(pathBoxItem.Path) ||
!storageItems.Any(storageItem =>
!string.IsNullOrEmpty(storageItem?.Path) &&
storageItem.Path.Replace(pathBoxItem.Path, string.Empty, StringComparison.Ordinal)
diff --git a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
index 3e6be428c906..c91f623ae7a9 100644
--- a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
@@ -1304,7 +1304,8 @@ private async Task HandleLocationItemDragOverAsync(LocationItem locationItem, It
(hasStorageItems && (storageItems.AreItemsAlreadyInFolder(path!) || storageItems.ContainsDestinationOrAncestor(path))) ||
path!.StartsWith("Home", StringComparison.OrdinalIgnoreCase) ||
path.StartsWith("ReleaseNotes", StringComparison.OrdinalIgnoreCase) ||
- path.StartsWith("Settings", StringComparison.OrdinalIgnoreCase))
+ path.StartsWith("Settings", StringComparison.OrdinalIgnoreCase) ||
+ (!OperatingSystem.IsWindows() && ZipStorageFolder.IsZipPath(path)))
{
rawEvent.AcceptedOperation = DataPackageOperation.None;
}
diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs
index 075db67fad24..7eb5bf423cd7 100644
--- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs
+++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs
@@ -1159,7 +1159,11 @@ private async void Item_DragOver(object sender, DragEventArgs e)
// Dropping onto an executable or a script opens the dragged items with it, so only the item itself is rejected there
var isOpenWithTarget = item.IsExecutable || item.IsScriptFile;
- if (isOpenWithTarget
+ if (!OperatingSystem.IsWindows() && ZipStorageFolder.IsZipPath(item.ItemPath))
+ {
+ e.AcceptedOperation = DataPackageOperation.None;
+ }
+ else if (isOpenWithTarget
? draggedItems.ContainsDestinationPath(item.ItemPath)
: draggedItems.ContainsDestinationOrAncestor(item.ItemPath))
{