Auxiliary tools for users of pixiv_down
Revision | 42f2bc4f33b51fe2b0d8903fe0b595c4f29fc1df (tree) |
---|---|
Time | 2023-01-29 12:27:49 |
Author | nemophila <stigma@disr...> |
Commiter | nemophila |
Add mlib/trash
Currently only works on Posix systems, though this isn't too
different from mlib/directories. Support for other operating
systems (namely macOS and Windows) should happen soon.
@@ -0,0 +1,312 @@ | ||
1 | +/* | |
2 | + * This is free and unencumbered software released into the public domain. | |
3 | + * | |
4 | + * Anyone is free to copy, modify, publish, use, compile, sell, or distribute this | |
5 | + * software, either in source code form or as a compiled binary, for any purpose, | |
6 | + * commercial or non-commercial, and by any means. | |
7 | + * | |
8 | + * In jurisdictions that recognize copyright laws, the author or authors of this | |
9 | + * software dedicate any and all copyright interest in the software to the public | |
10 | + * domain. We make this dedication for the benefit of the public at large and to | |
11 | + * the detriment of our heirs and successors. We intend this dedication to be an | |
12 | + * overt act of relinquishment in perpetuity of all present and future rights to | |
13 | + * this software under copyright law. | |
14 | + * | |
15 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
16 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
17 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
18 | + * AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN | |
19 | + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
20 | + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
21 | + */ | |
22 | + | |
23 | + | |
24 | +/** | |
25 | + * Common 'Trash' operations for the computers Recycle Bin. | |
26 | + * | |
27 | + * Only for POSIX systems (currently) and follows the XDG specification. | |
28 | + * | |
29 | + * Authors: mio <stigma@disroot.org> | |
30 | + * Date: January 29, 2023 | |
31 | + * License: public domain | |
32 | + * Standards: The FreeDesktop.org Trash Specification 1.0 | |
33 | + * Version: 0.1.0 | |
34 | + * | |
35 | + * Macros: | |
36 | + * DREF = <a href="https://dlang.org/phobos/$1.html#$2">$2</a> | |
37 | + * LREF = <a href="#$1">$1</a> | |
38 | + */ | |
39 | +module mlib.trash; | |
40 | + | |
41 | +import core.stdc.errno; | |
42 | + | |
43 | +import std.file; | |
44 | +import std.path; | |
45 | +import std.process : environment; | |
46 | +import std.stdio; | |
47 | + | |
48 | +/* | |
49 | + * Permanetely delete all trashed records. | |
50 | + * | |
51 | + * This currently throws an Exception as it's not yet implemented. | |
52 | + */ | |
53 | +// void emptyTrash() | |
54 | +// { | |
55 | +// throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented"); | |
56 | +// } | |
57 | + | |
58 | +/* | |
59 | + * Restore one (or all: "") trashed records | |
60 | + * | |
61 | + * Params: | |
62 | + * pathInTrash = The unique filename in the trash directory to | |
63 | + * restore. By not providing an argument (or by | |
64 | + * passing `""`) this will restore _all_ files. | |
65 | + * | |
66 | + * Note: This currently throws an Exception as it's not yet | |
67 | + * implemented. | |
68 | + */ | |
69 | +// void restoreTrash(string pathInTrash = "") | |
70 | +// { | |
71 | +// throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented"); | |
72 | +// } | |
73 | + | |
74 | +/* | |
75 | + * List all the files and directories currently inside the trash. | |
76 | + * | |
77 | + * Returns: A list of strings containing every filename in the trash. | |
78 | + * | |
79 | + * Note: This currently throws an Exception as it's not yet | |
80 | + * implemented. | |
81 | + */ | |
82 | +// string[] listTrash() | |
83 | +// { | |
84 | +// throw new Exception(__PRETTY_FUNCTION__ ~ " not implemented"); | |
85 | +// } | |
86 | + | |
87 | + | |
88 | +/** | |
89 | + * Trash the file or directory at *path*. | |
90 | + * | |
91 | + * Params: | |
92 | + * path = The path to move to the trash. | |
93 | + * | |
94 | + * Throws: | |
95 | + * - $(DREF std_file, FileException) if the file cannot be trashed. | |
96 | + */ | |
97 | +void trash(string path) | |
98 | +{ | |
99 | + scope string pathInTrash; | |
100 | + trash(path, pathInTrash); | |
101 | +} | |
102 | + | |
103 | +/** | |
104 | + * Trash the file or directory at *path*, and sets *pathInTrash* to the | |
105 | + * path at which the file can be found within the trash. | |
106 | + * | |
107 | + * Params: | |
108 | + * path = The path to move to the trash. | |
109 | + * pathInTrash = The path at which the newly trashed item can be found. | |
110 | + * | |
111 | + * Throws: | |
112 | + * - $(DREF std_file, FileException) if the file cannot be trashed. | |
113 | + */ | |
114 | +void trash(string path, out string pathInTrash) | |
115 | +{ | |
116 | + version (Posix) { | |
117 | + _posix_trash(path, pathInTrash); | |
118 | + } else { | |
119 | + throw new Exception(__PRETTY_FUNCTION__ ~ " is not supported on your OS"); | |
120 | + } | |
121 | +} | |
122 | + | |
123 | +/** | |
124 | + * Erase the file from the operating system. | |
125 | + * | |
126 | + * This skips the "trashing" operation and unlinks the file from the | |
127 | + * system and recovers the space. Files which have been erased are | |
128 | + * not recoverable. | |
129 | + * | |
130 | + * Throws: | |
131 | + * - $(DREF std_file, FileException) if the file cannot be removed. | |
132 | + */ | |
133 | +void erase(string path) | |
134 | +{ | |
135 | + // Really just a convenience function. | |
136 | + remove(path); | |
137 | +} | |
138 | + | |
139 | + | |
140 | +private: | |
141 | + | |
142 | +/* | |
143 | + * System independant functions. | |
144 | + * These will call the system specific function. | |
145 | + */ | |
146 | + | |
147 | +ulong getDevice(string path) { | |
148 | + version (Posix) { | |
149 | + return _posix_getDevice(path); | |
150 | + } | |
151 | +} | |
152 | + | |
153 | +string getHomeDirectory() { | |
154 | + version (Posix) { | |
155 | + return environment["HOME"]; | |
156 | + } | |
157 | +} | |
158 | + | |
159 | +bool isParent(string parent, string path) { | |
160 | + import std.string : startsWith; | |
161 | + | |
162 | + path = path.absolutePath; | |
163 | + parent = parent.absolutePath; | |
164 | + | |
165 | + return startsWith(path, parent); | |
166 | +} | |
167 | + | |
168 | +string getInfo(string src, string topdir) { | |
169 | + import std.uri : encode; | |
170 | + import std.datetime.systime : Clock; | |
171 | + | |
172 | + if (false == isParent(topdir, src)) { | |
173 | + src = src.absolutePath; | |
174 | + } else { | |
175 | + src = relativePath(src, topdir); | |
176 | + } | |
177 | + | |
178 | + string info = "[Trash Info]\n"; | |
179 | + info ~= "Path=" ~ encode(src) ~ "\n"; | |
180 | + | |
181 | + | |
182 | + /* | |
183 | + * Prior to D 2.099.0, the toISOExtString method didn't | |
184 | + * have a precision argument, which means it includes | |
185 | + * fractional seconds by default. So to accommodate | |
186 | + * for earlier versions, just trim it off. | |
187 | + */ | |
188 | + static if (__VERSION__ < 2099L) { | |
189 | + import std.string : split; | |
190 | + | |
191 | + string dateTime = Clock.currTime.toISOExtString().split(".")[0]; | |
192 | + | |
193 | + info ~= "DeletionDate=" ~ dateTime ~ "\n"; | |
194 | + } else { | |
195 | + info ~= "DeletionDate=" ~ Clock.currTime.toISOExtString(0) ~ "\n"; | |
196 | + } | |
197 | + | |
198 | + | |
199 | + return info; | |
200 | +} | |
201 | + | |
202 | +/* | |
203 | + * System specific implementation of the above functions. | |
204 | + */ | |
205 | + | |
206 | +version(Posix) { | |
207 | + | |
208 | + import core.sys.posix.sys.stat; | |
209 | + import std.conv : to; | |
210 | + import std.string : toStringz; | |
211 | + | |
212 | + void _posix_trash(string path, out string pathInTrash) { | |
213 | + if (false == exists(path)) { | |
214 | + throw new FileException(path, ENOENT); | |
215 | + } | |
216 | + | |
217 | + /* "When trashing a file or directory, the implementation SHOULD check | |
218 | + * whether the user has the necessary permissions to delete it, before | |
219 | + * starting the trashing operation itself". */ | |
220 | + uint attrs = getAttributes(path); | |
221 | + if (false == ((S_IRUSR & attrs) && (S_IWUSR & attrs))) { | |
222 | + throw new FileException(path, EACCES); | |
223 | + } | |
224 | + | |
225 | + ulong pathDev = getDevice(path); | |
226 | + ulong trashDev = getDevice(getHomeDirectory()); | |
227 | + | |
228 | + | |
229 | + // $topdir | |
230 | + string topdir; | |
231 | + // $trash | |
232 | + string trash; | |
233 | + | |
234 | + /* w.r.t. homeTrash: | |
235 | + * "Files that the user trashes from the same file system (device/partition) SHOULD | |
236 | + * be stored here ... If this directory is needed for a trashing operation but does | |
237 | + * not exist, the implementation SHOULD automatically create it, without warnings | |
238 | + * or delays. */ | |
239 | + if (pathDev == trashDev) { | |
240 | + topdir = _xdg_datahome(); | |
241 | + trash = buildPath(topdir, "Trash"); | |
242 | + } else { | |
243 | + /* "The implementation MAY also support trashing files from the rest of the | |
244 | + * system (including other partitions, shared network resources, and removable | |
245 | + * devices) into the "home trash" directory." | |
246 | + * | |
247 | + * I can only really test the partitions and removable devices, but I don't | |
248 | + * have my desktop setup with multiple partitions. Will check with removable | |
249 | + * devices, but want to see same file system usage work first. */ | |
250 | + throw new Exception("The device for the Trash directory and the device for the path are different."); | |
251 | + } | |
252 | + | |
253 | + string basename = baseName(path); | |
254 | + string filename = stripExtension(basename); | |
255 | + string ext = extension(basename); | |
256 | + | |
257 | + // $trash/files | |
258 | + string filesDir = buildPath(trash, "files"); | |
259 | + if (false == exists(filesDir)) { | |
260 | + mkdirRecurse(filesDir); | |
261 | + } | |
262 | + | |
263 | + // $trash/info | |
264 | + string infoDir = buildPath(trash, "info"); | |
265 | + if (false == exists(infoDir)) { | |
266 | + mkdirRecurse(infoDir); | |
267 | + } | |
268 | + | |
269 | + /* "The names in [$trash/files and $trash/info] are to be determined by the | |
270 | + * implementation; the only limitation is that they must be unique within the | |
271 | + * directory. Even if a file with the same name and location gets trashed many times, | |
272 | + * each subsequent trashing must not overwrite a previous copy." */ | |
273 | + size_t counter = 0; | |
274 | + string destname = basename; | |
275 | + string infoFilename = destname.setExtension(".trashinfo"); | |
276 | + while (exists(buildPath(filesDir, destname)) || exists(buildPath(infoDir, infoFilename))) { | |
277 | + counter += 1; | |
278 | + destname = filename ~ "_" ~ to!string(counter) ~ ext; | |
279 | + infoFilename = destname.setExtension(".trashinfo"); | |
280 | + } | |
281 | + | |
282 | + { | |
283 | + /* "When trashing a file or directory, the implementation MUST create the | |
284 | + * corresponding file in $trash/info first." */ | |
285 | + auto infoFile = File(buildPath(infoDir, infoFilename), "w"); | |
286 | + infoFile.write(getInfo(path, topdir)); | |
287 | + } | |
288 | + { | |
289 | + string filesPath = buildPath(filesDir, destname); | |
290 | + rename(path, filesPath); | |
291 | + pathInTrash = filesPath; | |
292 | + } | |
293 | + | |
294 | + /* TODO: Directory size cache */ | |
295 | + } | |
296 | + | |
297 | + ulong _posix_getDevice(string path) { | |
298 | + stat_t statbuf; | |
299 | + lstat(toStringz(path), &statbuf); | |
300 | + | |
301 | + return statbuf.st_dev; | |
302 | + } | |
303 | + | |
304 | + string _xdg_datahome() | |
305 | + { | |
306 | + if ("XDG_DATA_HOME" in environment) { | |
307 | + return environment["XDG_DATA_HOME"]; | |
308 | + } else { | |
309 | + return buildPath(environment["HOME"], ".local", "share"); | |
310 | + } | |
311 | + } | |
312 | +} // End of version(Posix) |