Files
DeskCore/Storage.cs
T
M_ViperandClaude Opus 5.5 77bb3e6b9d DeskCore 1.0.0
Desktop-Icons in eigene Kategorien sortieren: Windows-11-Design (hell/dunkel),
Desktop-Boxen, automatisches Einsortieren nach Regeln, Rückgängig, Suche,
Infobereich mit Tastenkürzel, Sicherung als ZIP und Installer ohne Adminrechte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:19:32 +02:00

333 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Concurrent;
using System.IO;
namespace DeskCore;
public enum TransferStatus { Ok, Skipped, Error }
/// <param name="Copied">true = kopiert (Original bleibt), false = verschoben</param>
public record TransferResult(TransferStatus Status, string Source, string? Dest = null, bool Copied = false, string? Message = null);
/// <summary>
/// Kategorien sind einfache Unterordner von %USERPROFILE%\DeskCore.
/// Icons vom Desktop werden dorthin kopiert – es bleiben normale Dateien.
/// </summary>
public static class Storage
{
public static readonly string Root = Migration.Pick(Environment.SpecialFolder.UserProfile);
public static readonly string UserDesktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
public static readonly string PublicDesktop = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
/// <summary>Hier liegen gelöschte Elemente, solange man sie noch rückgängig machen kann.</summary>
public static readonly string Holding = Migration.Pick(Environment.SpecialFolder.LocalApplicationData, "Geloescht");
// ---------- Kategorien ----------
public static List<string> Categories()
{
Directory.CreateDirectory(Root);
var order = AppSettings.Current.CategoryOrder;
return Directory.GetDirectories(Root)
.Select(Path.GetFileName)
.OfType<string>()
.OrderBy(n => order.FindIndex(o => string.Equals(o, n, StringComparison.OrdinalIgnoreCase)) is var i and >= 0 ? i : int.MaxValue)
.ThenBy(n => n, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
public static string CategoryPath(string name) => Path.Combine(Root, name);
public static IEnumerable<string> Entries(string dir)
{
if (!Directory.Exists(dir)) return [];
try
{
return Directory.EnumerateFileSystemEntries(dir)
.Where(p =>
{
var n = Path.GetFileName(p);
if (n.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase)) return false;
try
{
var attr = File.GetAttributes(p);
return !attr.HasFlag(FileAttributes.Hidden) && !attr.HasFlag(FileAttributes.System);
}
catch { return false; }
})
.ToList();
}
catch { return []; }
}
public static IEnumerable<string> DesktopEntries() =>
Entries(UserDesktop).Concat(Entries(PublicDesktop));
public static bool IsDesktopDir(string dir) =>
SamePath(dir, UserDesktop) || SamePath(dir, PublicDesktop);
public static bool IsInStorage(string path) =>
path.StartsWith(Root.TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
public static string? ValidateName(string name)
{
name = name.Trim();
if (name.Length == 0) return "Bitte einen Namen eingeben.";
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) return "Der Name enthält ungültige Zeichen ( \\ / : * ? \" < > | ).";
if (name is "." or ".." || name.EndsWith('.')) return "Ungültiger Name.";
return null;
}
// ---------- Duplikate ----------
private static readonly ConcurrentDictionary<string, (DateTime Stamp, string? Target)> _lnkCache = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Ziel einer Verknüpfung (.lnk/.url), sonst null.</summary>
public static string? ShortcutTarget(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
if (ext is not (".lnk" or ".url")) return null;
DateTime stamp;
try { stamp = File.GetLastWriteTimeUtc(path); } catch { return null; }
if (_lnkCache.TryGetValue(path, out var c) && c.Stamp == stamp) return c.Target;
string? target = null;
try
{
if (ext == ".url")
{
target = File.ReadLines(path).FirstOrDefault(l => l.StartsWith("URL=", StringComparison.OrdinalIgnoreCase))?[4..];
}
else
{
dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!;
dynamic lnk = shell.CreateShortcut(path);
string t = lnk.TargetPath;
string a = lnk.Arguments;
target = string.IsNullOrEmpty(t) ? null : (t + " " + a).Trim();
}
}
catch { }
if (string.IsNullOrWhiteSpace(target)) target = null;
_lnkCache[path] = (stamp, target);
return target;
}
/// <summary>Gleicher Dateiname oder Verknüpfung mit gleichem Ziel?</summary>
public static bool SameItem(string a, string b)
{
if (string.Equals(Path.GetFileName(a), Path.GetFileName(b), StringComparison.OrdinalIgnoreCase)) return true;
var ta = ShortcutTarget(a);
return ta is not null && string.Equals(ta, ShortcutTarget(b), StringComparison.OrdinalIgnoreCase);
}
/// <summary>Liegt das Element (oder ein gleichwertiges) schon in diesem Ordner?</summary>
public static string? FindEquivalent(string dir, string item) =>
Entries(dir).FirstOrDefault(e => !SamePath(e, item) && SameItem(e, item));
/// <summary>In welcher Kategorie liegt das Element bereits? (für das ✓ auf dem Desktop)</summary>
public static string? FindCategoryOf(string item, IReadOnlyDictionary<string, List<string>> categoryEntries)
{
foreach (var (cat, entries) in categoryEntries)
if (entries.Any(e => SameItem(e, item))) return cat;
return null;
}
// ---------- Übertragen ----------
/// <summary>
/// Übernimmt Datei/Ordner nach targetDir. Kommt die Quelle von außerhalb der App (z. B. vom Desktop),
/// wird kopiert – das Original kann danach gelöscht werden. Innerhalb der App und zurück auf den
/// Desktop wird verschoben. Gleiche Elemente werden nicht doppelt angelegt.
/// </summary>
public static TransferResult Transfer(string source, string targetDir, bool forceMove = false)
{
var name = Path.GetFileName(source.TrimEnd('\\'));
try
{
if (SamePath(Path.GetDirectoryName(source), targetDir))
return new(TransferStatus.Skipped, source, Message: $"„{name}“ ist schon dort.");
if (Directory.Exists(source) &&
(targetDir.TrimEnd('\\') + "\\").StartsWith(source.TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase))
return new(TransferStatus.Error, source, Message: $"„{name}“ kann nicht in sich selbst verschoben werden.");
bool copy = !forceMove && !IsInStorage(source) && IsInStorage(targetDir);
if (FindEquivalent(targetDir, source) is { } existing)
{
var where = IsDesktopDir(targetDir) ? "auf dem Desktop" : $"in „{Path.GetFileName(targetDir)}“";
return new(TransferStatus.Skipped, source, existing, Message: $"„{DisplayName(source)}“ ist schon {where}.");
}
Directory.CreateDirectory(targetDir);
var dest = UniquePath(targetDir, name);
if (copy) CopyAny(source, dest);
else MoveAny(source, dest);
return new(TransferStatus.Ok, source, dest, copy);
}
catch (UnauthorizedAccessException)
{
return new(TransferStatus.Error, source, Message: $"„{name}“: Keine Berechtigung. " +
"(Icons auf dem öffentlichen Desktop brauchen Administratorrechte – App ggf. als Administrator starten.)");
}
catch (Exception ex)
{
return new(TransferStatus.Error, source, Message: $"„{name}“: {ex.Message}");
}
}
/// <summary>Verschiebt Datei oder Ordner – auch zwischen Laufwerken.</summary>
public static void MoveAny(string source, string dest)
{
NoteCreated(dest);
if (File.Exists(source))
{
File.Move(source, dest);
return;
}
if (string.Equals(Path.GetPathRoot(source), Path.GetPathRoot(dest), StringComparison.OrdinalIgnoreCase))
{
Directory.Move(source, dest);
return;
}
CopyDirectory(source, dest);
Directory.Delete(source, recursive: true);
}
public static void CopyAny(string source, string dest)
{
NoteCreated(dest);
if (Directory.Exists(source)) CopyDirectory(source, dest);
else File.Copy(source, dest);
}
/// <summary>Benennt ein Element um. Gibt den neuen Pfad zurück.</summary>
public static string Rename(string path, string newName)
{
var dir = Path.GetDirectoryName(path)!;
var dest = Path.Combine(dir, newName);
if (string.Equals(path, dest, StringComparison.Ordinal)) return path;
if (string.Equals(path, dest, StringComparison.OrdinalIgnoreCase))
{ // nur Groß-/Kleinschreibung geändert
var tmp = path + ".umbenennen";
MoveAny(path, tmp);
MoveAny(tmp, dest);
return dest;
}
if (File.Exists(dest) || Directory.Exists(dest)) throw new IOException($"„{newName}“ gibt es dort schon.");
MoveAny(path, dest);
return dest;
}
/// <summary>
/// "Löscht" ein Element: es wandert erst in einen internen Ordner, damit Strg+Z funktioniert.
/// Erst wenn es nicht mehr rückgängig gemacht werden kann, geht es in den Papierkorb.
/// </summary>
public static string Delete(string path)
{
var dir = Path.Combine(Holding, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var dest = Path.Combine(dir, Path.GetFileName(path.TrimEnd('\\')));
MoveAny(path, dest);
return dest;
}
/// <summary>Endgültig in den Windows-Papierkorb.</summary>
public static void Recycle(string path)
{
try
{
if (Directory.Exists(path))
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(path,
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
else if (File.Exists(path))
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(path,
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
// leeren Hilfsordner aufräumen
var parent = Path.GetDirectoryName(path);
if (parent is not null && IsUnder(parent, Holding) && Directory.Exists(parent) && !Directory.EnumerateFileSystemEntries(parent).Any())
Directory.Delete(parent);
}
catch { }
}
/// <summary>Reste von einem Absturz in den Papierkorb legen.</summary>
public static void FlushHolding()
{
if (!Directory.Exists(Holding)) return;
foreach (var d in Directory.GetDirectories(Holding))
foreach (var e in Directory.EnumerateFileSystemEntries(d).ToList())
Recycle(e);
}
// ---------- Selbst angelegte Desktop-Elemente merken (damit die Auto-Sortierung sie ignoriert) ----------
private static readonly ConcurrentDictionary<string, DateTime> _created = new(StringComparer.OrdinalIgnoreCase);
private static void NoteCreated(string path) => _created[path] = DateTime.UtcNow;
public static bool CreatedByApp(string path) =>
_created.TryGetValue(path, out var t) && DateTime.UtcNow - t < TimeSpan.FromSeconds(15);
/// <summary>Größe eines Ordners in Bytes (bricht ab, sobald limit überschritten ist).</summary>
public static long DirectorySize(string dir, long limit = long.MaxValue)
{
long sum = 0;
try
{
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
{
try { sum += new FileInfo(f).Length; } catch { }
if (sum > limit) break;
}
}
catch { }
return sum;
}
// ---------- Helfer ----------
public static string DisplayName(string path)
{
var n = Path.GetFileName(path.TrimEnd('\\'));
var ext = Path.GetExtension(n).ToLowerInvariant();
return ext is ".lnk" or ".url" or ".appref-ms" ? Path.GetFileNameWithoutExtension(n) : n;
}
/// <summary>Endung, die beim Umbenennen erhalten bleibt (bei Verknüpfungen versteckt).</summary>
public static string HiddenExtension(string path)
{
var ext = Path.GetExtension(path);
return ext.ToLowerInvariant() is ".lnk" or ".url" or ".appref-ms" ? ext : "";
}
public static bool SamePath(string? a, string? b) =>
a is not null && b is not null &&
string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
public static bool IsUnder(string path, string dir) =>
(Path.GetFullPath(path).TrimEnd('\\') + "\\").StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
public static string UniquePath(string dir, string fileName)
{
var path = Path.Combine(dir, fileName);
if (!File.Exists(path) && !Directory.Exists(path)) return path;
var stem = Path.GetFileNameWithoutExtension(fileName);
var ext = Path.GetExtension(fileName);
for (int i = 2; ; i++)
{
path = Path.Combine(dir, $"{stem} ({i}){ext}");
if (!File.Exists(path) && !Directory.Exists(path)) return path;
}
}
private static void CopyDirectory(string source, string dest)
{
Directory.CreateDirectory(dest);
foreach (var f in Directory.GetFiles(source))
File.Copy(f, Path.Combine(dest, Path.GetFileName(f)));
foreach (var d in Directory.GetDirectories(source))
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)));
}
}