Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 22 additions & 65 deletions src/Files.App/Filesystem/FileTagsHelper.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using Common;
using Files.App.Filesystem.StorageItems;
using Files.App.Helpers;
using Files.App.Shell;
using Files.Shared.Extensions;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;
using Vanara.PInvoke;
using Windows.Storage;
using Windows.Storage.FileProperties;
using IO = System.IO;
Expand All @@ -18,76 +16,45 @@ public static class FileTagsHelper
{
public static string FileTagsDbPath => IO.Path.Combine(ApplicationData.Current.LocalFolder.Path, "filetags.db");

public static FileTagsDb GetDbInstance()
{
return new FileTagsDb(FileTagsDbPath);
}
private static readonly Lazy<FileTagsDb> dbInstance = new(() => new FileTagsDb(FileTagsDbPath, true));

public static FileTagsDb GetDbInstance() => dbInstance.Value;

public static string[] ReadFileTag(string filePath)
{
using var hStream = Kernel32.CreateFile($"{filePath}:files",
Kernel32.FileAccess.GENERIC_READ, 0, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (hStream.IsInvalid)
{
return null;
}
var bytes = new byte[4096];
var ret = Kernel32.ReadFile(hStream, bytes, (uint)bytes.Length, out var read, IntPtr.Zero);
if (!ret)
{
return null;
}
var tagString = System.Text.Encoding.UTF8.GetString(bytes, 0, (int)read);
return tagString.Split(',');
var tagString = NativeFileOperationsHelper.ReadStringFromFile($"{filePath}:files");
return tagString?.Split(',');
}

public static bool WriteFileTag(string filePath, string[] tag)
public static void WriteFileTag(string filePath, string[] tag)
{
var dateOk = GetFileDateModified(filePath, out var dateModified); // Backup date modified
bool result = false;
if (tag is null || !tag.Any())
var isDateOk = NativeFileOperationsHelper.GetFileDateModified(filePath, out var dateModified); // Backup date modified
var isReadOnly = NativeFileOperationsHelper.HasFileAttribute(filePath, IO.FileAttributes.ReadOnly);
if (isReadOnly) // Unset read-only attribute (#7534)
{
result = Kernel32.DeleteFile($"{filePath}:files");
NativeFileOperationsHelper.UnsetFileAttribute(filePath, IO.FileAttributes.ReadOnly);
}
else
if (tag is null || !tag.Any())
{
using var hStream = Kernel32.CreateFile($"{filePath}:files",
Kernel32.FileAccess.GENERIC_WRITE, 0, null, FileMode.Create, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (hStream.IsInvalid)
{
return false;
}
byte[] buff = System.Text.Encoding.UTF8.GetBytes(string.Join(',', tag));
result = Kernel32.WriteFile(hStream, buff, (uint)buff.Length, out var written, IntPtr.Zero);
NativeFileOperationsHelper.DeleteFileFromApp($"{filePath}:files");
}
if (dateOk)
else if (ReadFileTag(filePath) is not string[] arr || !tag.SequenceEqual(arr))
{
SetFileDateModified(filePath, dateModified); // Restore date modified
NativeFileOperationsHelper.WriteStringToFile($"{filePath}:files", string.Join(',', tag));
}
return result;
}

public static ulong? GetFileFRN(string filePath)
{
//using var si = new ShellItem(filePath);
//return (ulong?)si.Properties["System.FileFRN"]; // Leaves open file handles
using var hFile = Kernel32.CreateFile(filePath, Kernel32.FileAccess.GENERIC_READ, FileShare.ReadWrite, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS);
if (hFile.IsInvalid)
if (isReadOnly) // Restore read-only attribute (#7534)
{
return null;
NativeFileOperationsHelper.SetFileAttribute(filePath, IO.FileAttributes.ReadOnly);
}
ulong? frn = null;
SafetyExtensions.IgnoreExceptions(() =>
if (isDateOk)
{
var fileID = Kernel32.GetFileInformationByHandleEx<Kernel32.FILE_ID_INFO>(hFile, Kernel32.FILE_INFO_BY_HANDLE_CLASS.FileIdInfo);
frn = BitConverter.ToUInt64(fileID.FileId.Identifier, 0);
});
return frn;
NativeFileOperationsHelper.SetFileDateModified(filePath, dateModified); // Restore date modified
}
}

public static void UpdateTagsDb()
{
using var dbInstance = GetDbInstance();
var dbInstance = GetDbInstance();
foreach (var file in dbInstance.GetAll())
{
var pathFromFrn = Win32API.PathFromFileId(file.Frn ?? 0, file.FilePath);
Expand Down Expand Up @@ -128,17 +95,7 @@ public static void UpdateTagsDb()
}
}

private static bool GetFileDateModified(string filePath, out FILETIME dateModified)
{
using var hFile = Kernel32.CreateFile(filePath, Kernel32.FileAccess.GENERIC_READ, FileShare.Read, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS);
return Kernel32.GetFileTime(hFile, out _, out _, out dateModified);
}

private static bool SetFileDateModified(string filePath, FILETIME dateModified)
{
using var hFile = Kernel32.CreateFile(filePath, Kernel32.FileAccess.FILE_WRITE_ATTRIBUTES, FileShare.None, null, FileMode.Open, FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS);
return Kernel32.SetFileTime(hFile, new(), new(), dateModified);
}
public static ulong? GetFileFRN(string filePath) => NativeFileOperationsHelper.GetFileFRN(filePath);

public static Task<ulong?> GetFileFRN(IStorageItem item)
{
Expand Down
6 changes: 2 additions & 4 deletions src/Files.App/Filesystem/ListedItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,8 @@ public string[] FileTags
{
if (SetProperty(ref fileTags, value))
{
using (var dbInstance = FileTagsHelper.GetDbInstance())
{
dbInstance.SetTags(ItemPath, FileFRN, value);
}
var dbInstance = FileTagsHelper.GetDbInstance();
dbInstance.SetTags(ItemPath, FileFRN, value);
FileTagsHelper.WriteFileTag(ItemPath, value);
OnPropertyChanged(nameof(FileTagsUI));
}
Expand Down
10 changes: 4 additions & 6 deletions src/Files.App/Filesystem/Search/FolderSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,10 @@ private async Task SearchTagsAsync(string folder, IList<ListedItem> results, Can
{
return;
}
List<Common.FileTagsDb.TaggedFile>? matches;
using (var dbInstance = FileTagsHelper.GetDbInstance())
{
matches = dbInstance.GetAllUnderPath(folder)
.Where(x => tags.All(x.Tags.Contains)).ToList();
}

var dbInstance = FileTagsHelper.GetDbInstance();
var matches = dbInstance.GetAllUnderPath(folder)
.Where(x => tags.All(x.Tags.Contains));

foreach (var match in matches)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/Helpers/FileOperationsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ public static bool SetCompatOptions(string filePath, string options)

private static void UpdateFileTagsDb(ShellFileOperations.ShellFileOpEventArgs e, string operationType)
{
using var dbInstance = FileTagsHelper.GetDbInstance();
var dbInstance = FileTagsHelper.GetDbInstance();
if (e.Result.Succeeded)
{
var sourcePath = e.SourceItem.GetParsingPath();
Expand Down
87 changes: 32 additions & 55 deletions src/Files.App/Helpers/LayoutPreferences/LayoutPrefsDb.cs
Original file line number Diff line number Diff line change
@@ -1,66 +1,25 @@
using Files.Shared.Extensions;
using LiteDB;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Threading;
using JsonSerializer = System.Text.Json.JsonSerializer;
using System.Linq;
using System.Text;
using IO = System.IO;

namespace Files.App.Helpers.LayoutPreferences
{
public class LayoutPrefsDb : IDisposable
{
private readonly LiteDatabase db;
private readonly IEnumerator mutexCoroutine;
private static readonly Mutex dbMutex = new(false, "Files_LayoutSettingsDb");
private static readonly ConcurrentQueue<Action> backgroundMutexOperationQueue = new();

static LayoutPrefsDb()
public LayoutPrefsDb(string connection, bool shared = false)
{
new Thread(OperationQueueWorker) { IsBackground = true }.Start();
}

private static void OperationQueueWorker()
{
while (!Environment.HasShutdownStarted)
{
SpinWait.SpinUntil(() => !backgroundMutexOperationQueue.IsEmpty);
while (backgroundMutexOperationQueue.TryDequeue(out var action))
{
action();
}
}
}

public LayoutPrefsDb(string connection)
{
mutexCoroutine = MutexOperator().GetEnumerator();
mutexCoroutine.MoveNext();
SafetyExtensions.IgnoreExceptions(() => CheckDbVersion(connection));
db = new LiteDatabase(new ConnectionString(connection)
{
Connection = ConnectionType.Direct,
Upgrade = true
Mode = shared ? LiteDB.FileMode.Shared : LiteDB.FileMode.Exclusive
}, new BsonMapper() { IncludeFields = true });
}

private IEnumerable MutexOperator()
{
var e1 = new ManualResetEventSlim();
var e2 = new ManualResetEventSlim();
backgroundMutexOperationQueue.Enqueue(() =>
{
dbMutex.WaitOne();
e1.Set();
e2.Wait();
e2.Dispose();
dbMutex.ReleaseMutex();
});
e1.Wait();
e1.Dispose();
yield return default;
e2.Set();
}

public void SetPreferences(string filePath, ulong? frn, LayoutPreferences? prefs)
{
// Get a collection (or create, if doesn't exist)
Expand Down Expand Up @@ -145,11 +104,11 @@ public void ResetAll(Func<LayoutDbPrefs, bool>? predicate = null)
var col = db.GetCollection<LayoutDbPrefs>("layoutprefs");
if (predicate is null)
{
col.DeleteAll();
col.Delete(Query.All());
}
else
{
col.DeleteMany(x => predicate(x));
col.Delete(x => predicate(x));
}
}

Expand All @@ -169,20 +128,38 @@ public void ApplyToAll(Action<LayoutDbPrefs> updateAction, Func<LayoutDbPrefs, b
public void Dispose()
{
db.Dispose();
mutexCoroutine.MoveNext();
}

public void Import(string json)
{
var dataValues = JsonSerializer.Deserialize<LayoutDbPrefs[]>(json);
var col = db.GetCollection<LayoutDbPrefs>("layoutprefs");
col.DeleteAll();
col.InsertBulk(dataValues);
var dataValues = JsonSerializer.DeserializeArray(json);
var col = db.GetCollection("layoutprefs");
col.Delete(Query.All());
col.InsertBulk(dataValues.Select(x => x.AsDocument));
}

public string Export()
{
return JsonSerializer.Serialize(db.GetCollection<LayoutDbPrefs>("layoutprefs").FindAll());
return JsonSerializer.Serialize(new BsonArray(db.GetCollection("layoutprefs").FindAll()));
}

// https://github.com/mbdavid/LiteDB/blob/master/LiteDB/Engine/Engine/Upgrade.cs
private void CheckDbVersion(string filename)
{
var buffer = new byte[8192 * 2];
using (var stream = new IO.FileStream(filename, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.ReadWrite))
{
// read first 16k
stream.Read(buffer, 0, buffer.Length);

// checks if v7 (plain or encrypted)
if (Encoding.UTF8.GetString(buffer, 25, "** This is a LiteDB file **".Length) == "** This is a LiteDB file **" &&
buffer[52] == 7)
{
return; // version 4.1.4
}
}
IO.File.Delete(filename); // recreate DB with correct version
}

public class LayoutDbPrefs
Expand Down
17 changes: 17 additions & 0 deletions src/Files.App/Helpers/NativeFileOperationsHelper.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Files.Shared.Extensions;
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
Expand All @@ -6,6 +7,7 @@
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Threading;
using Vanara.PInvoke;

namespace Files.App.Helpers
{
Expand Down Expand Up @@ -459,6 +461,21 @@ public static bool WriteBufferToFileWithProgress(string filePath, byte[] buffer,
return null;
}

public static ulong? GetFileFRN(string filePath)
{
using var handle = OpenFileForRead(filePath);
if (!handle.IsInvalid)
{
try
{
var fileID = Kernel32.GetFileInformationByHandleEx<Kernel32.FILE_ID_INFO>(handle, Kernel32.FILE_INFO_BY_HANDLE_CLASS.FileIdInfo);
return BitConverter.ToUInt64(fileID.FileId.Identifier, 0);
}
catch { }
}
return null;
}

// https://github.com/rad1oactive/BetterExplorer/blob/master/Windows%20API%20Code%20Pack%201.1/source/WindowsAPICodePack/Shell/ReparsePoint.cs
public static string ParseSymLink(string path)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Files.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ async Task PerformNavigation(string payload, string selectItem = null)
if (fileFRN is not null)
{
var tagUid = tag is not null ? new[] { tag.Uid } : null;
using var dbInstance = FileTagsHelper.GetDbInstance();
var dbInstance = FileTagsHelper.GetDbInstance();
dbInstance.SetTags(file, fileFRN, tagUid);
FileTagsHelper.WriteFileTag(file, tagUid);
}
Expand Down
13 changes: 7 additions & 6 deletions src/Files.App/ViewModels/FolderSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ namespace Files.App.ViewModels
public class FolderSettingsViewModel : ObservableObject
{
public static string LayoutSettingsDbPath => IO.Path.Combine(ApplicationData.Current.LocalFolder.Path, "user_settings.db");
public static LayoutPrefsDb GetDbInstance()
{
return new LayoutPrefsDb(LayoutSettingsDbPath);
}

private static readonly Lazy<LayoutPrefsDb> dbInstance = new(() => new LayoutPrefsDb(LayoutSettingsDbPath, true));

public static LayoutPrefsDb GetDbInstance() => dbInstance.Value;

public event EventHandler<LayoutPreferenceEventArgs>? LayoutPreferencesUpdateRequired;

Expand Down Expand Up @@ -358,7 +358,8 @@ private static LayoutPreferences ReadLayoutPreferencesFromAds(string folderPath,
{
if (string.IsNullOrEmpty(folderPath))
return null;
using var dbInstance = GetDbInstance();

var dbInstance = GetDbInstance();
return dbInstance.GetPreferences(folderPath, frn);
}

Expand All @@ -382,7 +383,7 @@ private static void WriteLayoutPreferencesToDb(string folderPath, ulong? frn, La
if (string.IsNullOrEmpty(folderPath))
return;

using var dbInstance = GetDbInstance();
var dbInstance = GetDbInstance();
if (dbInstance.GetPreferences(folderPath, frn) is null)
{
if (LayoutPreferences.DefaultLayoutPreferences.Equals(prefs))
Expand Down
6 changes: 2 additions & 4 deletions src/Files.App/ViewModels/ItemViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -983,10 +983,8 @@ await dispatcherQueue.EnqueueAsync(async () =>

private static void SetFileTag(ListedItem item)
{
using (var dbInstance = FileTagsHelper.GetDbInstance())
{
dbInstance.SetTags(item.ItemPath, item.FileFRN, item.FileTags);
}
var dbInstance = FileTagsHelper.GetDbInstance();
dbInstance.SetTags(item.ItemPath, item.FileFRN, item.FileTags);
}

// This works for recycle bin as well as GetFileFromPathAsync/GetFolderFromPathAsync work
Expand Down
Loading