|
5 | 5 | using System.Threading; |
6 | 6 | using System.Threading.Tasks; |
7 | 7 |
|
8 | | -namespace YoutubeDl.Wpf.Utils |
| 8 | +namespace YoutubeDl.Wpf.Utils; |
| 9 | + |
| 10 | +public static class FileHelper |
9 | 11 | { |
10 | | - public static class FileHelper |
11 | | - { |
12 | | - public static readonly string configDirectory; |
| 12 | + private static readonly string s_configDirectory; |
13 | 13 |
|
14 | | - static FileHelper() |
15 | | - { |
| 14 | + static FileHelper() |
| 15 | + { |
16 | 16 | #if PACKAGED |
17 | | - // ~/.config on Linux |
18 | | - // ~/AppData/Roaming on Windows |
19 | | - var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); |
20 | | - configDirectory = $"{appDataPath}/youtube-dl-wpf"; |
| 17 | + // ~/.config on Linux |
| 18 | + // ~/AppData/Roaming on Windows |
| 19 | + var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); |
| 20 | + s_configDirectory = Path.Join(appDataPath, "youtube-dl-wpf"); |
21 | 21 | #else |
22 | | - // Use executable directory |
23 | | - // Executable directory for single-file deployments in .NET 5: https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file |
24 | | - configDirectory = AppContext.BaseDirectory; |
| 22 | + // Use executable directory |
| 23 | + // Executable directory for single-file deployments in .NET 5+: https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file |
| 24 | + s_configDirectory = AppContext.BaseDirectory; |
25 | 25 | #endif |
26 | | - } |
| 26 | + } |
27 | 27 |
|
28 | | - /// <summary> |
29 | | - /// Gets the fully qualified absolute path |
30 | | - /// that the original path points to. |
31 | | - /// </summary> |
32 | | - /// <param name="path">A relative or absolute path.</param> |
33 | | - /// <returns>A fully qualified path.</returns> |
34 | | - public static string GetAbsolutePath(string path) |
35 | | - => Path.IsPathFullyQualified(path) ? path : $"{configDirectory}/{path}"; |
| 28 | + /// <summary> |
| 29 | + /// Gets the absolute path pointed to by the specified path. |
| 30 | + /// </summary> |
| 31 | + /// <param name="path">A relative or absolute path.</param> |
| 32 | + /// <returns>A fully qualified path.</returns> |
| 33 | + public static string GetAbsolutePath(string path) => Path.Combine(s_configDirectory, path); |
| 34 | + |
| 35 | + /// <summary> |
| 36 | + /// Loads the specified JSON file and deserializes its content as a <typeparamref name="TValue"/>. |
| 37 | + /// </summary> |
| 38 | + /// <typeparam name="TValue">The type to deserialize the JSON value into.</typeparam> |
| 39 | + /// <param name="path">JSON file path.</param> |
| 40 | + /// <param name="jsonTypeInfo">Metadata about the type to convert.</param> |
| 41 | + /// <param name="cancellationToken">A token that may be used to cancel the read operation.</param> |
| 42 | + /// <returns>A <typeparamref name="TValue"/>.</returns> |
| 43 | + public static async Task<TValue> LoadFromJsonFileAsync<TValue>(string path, JsonTypeInfo<TValue> jsonTypeInfo, CancellationToken cancellationToken = default) where TValue : class, new() |
| 44 | + { |
| 45 | + path = GetAbsolutePath(path); |
| 46 | + if (!File.Exists(path)) |
| 47 | + return new(); |
36 | 48 |
|
37 | | - /// <summary> |
38 | | - /// Loads data from a JSON file. |
39 | | - /// </summary> |
40 | | - /// <typeparam name="T">Data object type.</typeparam> |
41 | | - /// <param name="filename">JSON file name.</param> |
42 | | - /// <param name="jsonTypeInfo">Metadata about the type to convert.</param> |
43 | | - /// <param name="cancellationToken">A token that may be used to cancel the read operation.</param> |
44 | | - /// <returns> |
45 | | - /// A ValueTuple containing a data object loaded from the JSON file and an error message. |
46 | | - /// The error message is null if no errors occurred. |
47 | | - /// </returns> |
48 | | - public static async Task<(T, string? errMsg)> LoadJsonAsync<T>(string filename, JsonTypeInfo<T> jsonTypeInfo, CancellationToken cancellationToken = default) where T : class, new() |
| 49 | + var fileStream = new FileStream(path, FileMode.Open); |
| 50 | + await using (fileStream.ConfigureAwait(false)) |
49 | 51 | { |
50 | | - // extend relative path |
51 | | - filename = GetAbsolutePath(filename); |
52 | | - |
53 | | - if (!File.Exists(filename)) |
54 | | - return (new(), null); |
55 | | - |
56 | | - if (cancellationToken.IsCancellationRequested) |
57 | | - return (new(), "The operation was canceled."); |
58 | | - |
59 | | - T? jsonData = null; |
60 | | - string? errMsg = null; |
61 | | - FileStream? jsonFile = null; |
62 | | - |
63 | | - try |
64 | | - { |
65 | | - jsonFile = new(filename, FileMode.Open); |
66 | | - jsonData = await JsonSerializer.DeserializeAsync<T>(jsonFile, jsonTypeInfo, cancellationToken).ConfigureAwait(false); |
67 | | - } |
68 | | - catch (Exception ex) |
69 | | - { |
70 | | - errMsg = $"Error: failed to load {filename}: {ex.Message}"; |
71 | | - } |
72 | | - finally |
73 | | - { |
74 | | - if (jsonFile is not null) |
75 | | - await jsonFile.DisposeAsync().ConfigureAwait(false); |
76 | | - } |
77 | | - |
78 | | - jsonData ??= new(); |
79 | | - return (jsonData, errMsg); |
| 52 | + return await JsonSerializer.DeserializeAsync(fileStream, jsonTypeInfo, cancellationToken).ConfigureAwait(false) ?? new(); |
80 | 53 | } |
| 54 | + } |
81 | 55 |
|
82 | | - /// <summary> |
83 | | - /// Saves data to a JSON file. |
84 | | - /// </summary> |
85 | | - /// <typeparam name="T">Data object type.</typeparam> |
86 | | - /// <param name="filename">JSON file name.</param> |
87 | | - /// <param name="jsonData">The data object to save.</param> |
88 | | - /// <param name="jsonTypeInfo">Metadata about the type to convert.</param> |
89 | | - /// <param name="alwaysOverwrite">Always overwrite the original file.</param> |
90 | | - /// <param name="noBackup">Do not create `filename.old` as backup.</param> |
91 | | - /// <param name="cancellationToken">A token that may be used to cancel the write operation.</param> |
92 | | - /// <returns>An error message. Null if no errors occurred.</returns> |
93 | | - public static async Task<string?> SaveJsonAsync<T>( |
94 | | - string filename, |
95 | | - T jsonData, |
96 | | - JsonTypeInfo<T> jsonTypeInfo, |
97 | | - bool alwaysOverwrite = false, |
98 | | - bool noBackup = false, |
99 | | - CancellationToken cancellationToken = default) |
100 | | - { |
101 | | - // extend relative path |
102 | | - filename = GetAbsolutePath(filename); |
103 | | - |
104 | | - string? errMsg = null; |
105 | | - FileStream? jsonFile = null; |
106 | | - |
107 | | - try |
108 | | - { |
109 | | - if (cancellationToken.IsCancellationRequested) |
110 | | - return "The operation was canceled."; |
111 | | - |
112 | | - // create directory |
113 | | - var directoryPath = Path.GetDirectoryName(filename); |
114 | | - if (directoryPath is null) |
115 | | - return $"Error: invalid path: {filename}"; |
| 56 | + /// <summary> |
| 57 | + /// Serializes the provided value as JSON and saves to the specified file. |
| 58 | + /// </summary> |
| 59 | + /// <typeparam name="TValue">The type of the value to serialize.</typeparam> |
| 60 | + /// <param name="path">JSON file path.</param> |
| 61 | + /// <param name="value">The value to save.</param> |
| 62 | + /// <param name="jsonTypeInfo">Metadata about the type to convert.</param> |
| 63 | + /// <param name="cancellationToken">A token that may be used to cancel the write operation.</param> |
| 64 | + /// <returns>A task that represents the asynchronous write operation.</returns> |
| 65 | + public static async Task SaveToJsonFileAsync<TValue>( |
| 66 | + string path, |
| 67 | + TValue value, |
| 68 | + JsonTypeInfo<TValue> jsonTypeInfo, |
| 69 | + CancellationToken cancellationToken = default) |
| 70 | + { |
| 71 | + path = GetAbsolutePath(path); |
116 | 72 |
|
117 | | - Directory.CreateDirectory(directoryPath); |
| 73 | + var directoryPath = Path.GetDirectoryName(path); |
| 74 | + if (string.IsNullOrEmpty(directoryPath)) |
| 75 | + throw new ArgumentException("Invalid path.", nameof(path)); |
118 | 76 |
|
119 | | - // save JSON |
120 | | - if (alwaysOverwrite || !File.Exists(filename)) // alwaysOverwrite or file doesn't exist. Just write to it. |
121 | | - { |
122 | | - jsonFile = new(filename, FileMode.Create); |
123 | | - await JsonSerializer.SerializeAsync(jsonFile, jsonData, jsonTypeInfo, cancellationToken).ConfigureAwait(false); |
124 | | - } |
125 | | - else // File exists. Write to `filename.new` and then replace with the new file and creates backup `filename.old`. |
126 | | - { |
127 | | - jsonFile = new($"{filename}.new", FileMode.Create); |
128 | | - await JsonSerializer.SerializeAsync(jsonFile, jsonData, jsonTypeInfo, cancellationToken).ConfigureAwait(false); |
129 | | - jsonFile.Close(); |
130 | | - File.Replace($"{filename}.new", filename, noBackup ? null : $"{filename}.old"); |
131 | | - } |
132 | | - } |
133 | | - catch (Exception ex) |
134 | | - { |
135 | | - errMsg = $"Error: failed to save {filename}: {ex.Message}"; |
136 | | - } |
137 | | - finally |
138 | | - { |
139 | | - if (jsonFile is not null) |
140 | | - await jsonFile.DisposeAsync().ConfigureAwait(false); |
141 | | - } |
| 77 | + _ = Directory.CreateDirectory(directoryPath); |
142 | 78 |
|
143 | | - return errMsg; |
| 79 | + var newPath = $"{path}.new"; |
| 80 | + var fileStream = new FileStream(newPath, FileMode.Create); |
| 81 | + await using (fileStream.ConfigureAwait(false)) |
| 82 | + { |
| 83 | + await JsonSerializer.SerializeAsync(fileStream, value, jsonTypeInfo, cancellationToken); |
144 | 84 | } |
| 85 | + File.Replace(newPath, path, $"{path}.old"); |
145 | 86 | } |
146 | 87 | } |
0 commit comments