1 /// Provides implementation of file operation with asynchronous IO.
2 module dpromise.utils.file;
3 
4 import dpromise.promise;
5 import std.range.primitives, std.traits;
6 import std.file;
7 import deimos.libuv.uv, dpromise.internal.libuv;
8 
9 
10 /**
11 Writes $(D buffer) to file $(D path) with asynchronous IO.
12 
13 Creates a new file if it is not already exist.
14 If an error occurred, the promise will be rejected.
15 
16 Params:
17   path = string repreesenting file path.
18   buffer = data to be written to file.
19 
20 See_Also: $(HTTP https://dlang.org/phobos/std_file.html#.write, std.file.write)
21 */
22 nothrow Promise!void writeAsync(string path, in void[] buffer) {
23   return promise!void((res, rej) {
24     writeAsyncWithCallback(path, buffer, (e) nothrow {
25       e is null ? res() : rej(e);
26     });
27   });
28 }
29 
30 ///
31 @system unittest {
32   import std.file : exists, readText, remove;
33   import dpromise.utils : runEventloop;
34 
35   writeAsync("hoge.txt", "hogehogepiyopiyo").then({
36     assert(exists("hoge.txt"));
37     assert(readText("hoge.txt") == "hogehogepiyopiyo");
38     remove("hoge.txt");
39   });
40 
41   runEventloop();
42 }
43 
44 
45 /**
46 Writes $(D buffer) to file $(D path) then calls the $(D callback) function with data as untyped array.
47 
48 Creates a new file if it is not already exist.
49 If an error occurred, the $(D callback) function will be called with the error.
50 
51 Params:
52  path = string repreesenting file path.
53  buffer = data to be written to file.
54  callback = a fuction called when operation finished or an error occurred.
55 */
56 nothrow @safe void writeAsyncWithCallback(string path, in void[] buffer, void delegate(Exception) nothrow callback)
57 in {
58   assert(callback !is null);
59 } body {
60   struct Data {
61     int err;
62     int handle;
63     void delegate(Exception) nothrow callback;
64     void[] buf;
65   }
66 
67   extern(C) @trusted nothrow static void ret(uv_fs_t* req) {
68     auto data = cast(Data*)req.data;
69     auto callback = data.callback;
70     callback(factory(data.err));
71 
72     scope(exit) {
73       uv_fs_req_cleanup(req);
74       import core.memory : GC;
75       import core.stdc.stdlib : free;
76       GC.removeRoot(callback.ptr);
77       free(req.data);
78       free(req);
79     }
80   }
81 
82   extern(C) @trusted nothrow static void on_close(uv_fs_t* req) {
83     auto result = cast(int)req.result;
84     auto data = cast(Data*)req.data;
85     if(result < 0) {
86       data.err = result;
87     }
88     ret(req);
89   }
90 
91   extern(C) @trusted nothrow static void on_write(uv_fs_t* req) {
92     auto result = cast(int)req.result;
93     auto data = cast(Data*)req.data;
94     if(result < 0) {
95       data.err = result;
96       ret(req);
97       return;
98     }
99 
100     uv_fs_req_cleanup(req);
101 
102     data.err = uv_fs_close(localLoop, req, data.handle, &on_close);
103     if(data.err != 0) ret(req);
104   }
105 
106   extern(C) @trusted nothrow static void on_open(uv_fs_t* req) {
107     auto result = cast(int)req.result;
108     auto data = cast(Data*)req.data;
109     if(result < 0) {
110       data.err = result;
111       ret(req);
112       return;
113     } else {
114       data.handle = result;
115     }
116 
117     uv_fs_req_cleanup(req);
118 
119     auto iov = uv_buf_init(cast(char*)(data.buf.ptr), cast(uint)data.buf.length);
120     data.err = uv_fs_write(localLoop, req, data.handle, &iov, 1, 0, &on_write);
121     if(data.err != 0) ret(req);
122   }
123 
124   () @trusted nothrow {
125     import core.memory : GC;
126     import std..string : toStringz;
127     auto req = castMalloc!(uv_fs_t);
128     auto data = castMalloc!Data;
129     GC.addRoot(callback.ptr);
130     data.callback = callback;
131     data.buf = cast(void[])buffer;
132     req.data = data;
133 
134     data.err = uv_fs_open(localLoop, req, path.toStringz, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR | S_IRGRP | S_IROTH, &on_open);
135     if(data.err != 0) ret(req);
136   }();
137 }
138 
139 ///
140 @safe unittest {
141   import std.file : exists, readText, remove;
142   import dpromise.utils : runEventloop;
143   writeAsyncWithCallback("hoge.txt", "hogehogepiyopiyo", (e) nothrow {
144     try {
145       assert(e is null);
146       assert(exists("hoge.txt"));
147       assert(readText("hoge.txt") == "hogehogepiyopiyo");
148 
149       scope(exit) {
150         remove("hoge.txt");
151       }
152     } catch(Exception e) {}
153   });
154 
155   runEventloop();
156 }
157 
158 
159 /**
160 Read file $(D path) with asynchronous IO and returns data read as a promise of untyped array.
161 If file size is larger than $(D up_to), only $(D up_to) bytes read.
162 
163 If an error occurred, the promise will be rejected.
164 
165 Params:
166   path = string repreesenting file path.
167   up_to = max buffer size. default value is $(D size_t.max).
168 
169 Returns: Promise of untyped array of bytes read
170 
171 See_Also: $(HTTP https://dlang.org/phobos/std_file.html#.read, std.file.read)
172 */
173 nothrow Promise!(void[]) readAsync(string path, size_t up_to = size_t.max) {
174   return promise!(void[])((res, rej) nothrow {
175     readAsyncWithCallback(path, up_to, (data, e) nothrow {
176       e is null ? res(data) : rej(e);
177     });
178   });
179 }
180 
181 ///
182 @system unittest {
183   import std.file : write, remove;
184   import dpromise.utils : runEventloop;
185 
186   write("hoge.txt", "hogehogepiyopiyo");
187   scope(exit) remove("hoge.txt");
188 
189   readAsync("hoge.txt").then(
190     (data) {
191       assert(data !is null);
192       assert(cast(string)data == "hogehogepiyopiyo");
193     }
194   );
195 
196   readAsync("hoge.txt", 8).then(
197     (data) {
198       assert(data !is null);
199       assert(cast(string)data == "hogehoge");
200     }
201   );
202 
203   runEventloop();
204 }
205 
206 
207 /**
208 Read file $(D path) with asynchronous IO then calls the $(D callback) function.
209 If file size is larger than $(D up_to), only $(D up_to) bytes read.
210 
211 If an error occurred, the $(D callback) function will be called with the error.
212 
213 Params:
214   path = string repreesenting file path.
215   up_to = max buffer size. default value is $(D size_t.max).
216   callback = a fuction called when operation finished or an error occurred.
217 */
218 nothrow @safe void readAsyncWithCallback(string path, size_t up_to, void delegate(void[], Exception) nothrow callback)
219 in {
220   assert(callback !is null);
221 } body {
222   struct Data {
223     int err;
224     int handle;
225     void delegate(void[], Exception) nothrow callback;
226     size_t up_to;
227     ubyte[] buf;
228   }
229 
230   extern(C) nothrow @trusted static void ret(uv_fs_t* req) {
231     auto data = cast(Data*)(req.data);
232     auto e = factory(data.err);
233 
234     if(e !is null) data.buf = null;
235 
236     data.callback(cast(void[])(data.buf), e);
237 
238     scope(exit) {
239       import core.memory : GC;
240       import core.stdc.stdlib : free;
241       GC.removeRoot(data.callback.ptr);
242       uv_fs_req_cleanup(req);
243       free(data);
244       free(req);
245     }
246   }
247 
248   extern(C) nothrow @trusted static void on_read(uv_fs_t* req) {
249     auto result = cast(int)req.result;
250     auto data = cast(Data*)req.data;
251     if(result < 0) {
252       data.err = result;
253       ret(req);
254       return;
255     }
256 
257     uv_fs_req_cleanup(req);
258 
259     data.err = uv_fs_close(localLoop, req, data.handle, &ret);
260     if(data.err != 0) ret(req);
261   }
262 
263   extern(C) nothrow @trusted static void on_stat(uv_fs_t* req) {
264     auto result = cast(int)req.result;
265     auto data = cast(Data*)req.data;
266     immutable size = req.statbuf.st_size;
267     if(result < 0) {
268       data.err = result;
269       ret(req);
270       return;
271     }
272 
273     uv_fs_req_cleanup(req);
274 
275     auto buf = new ubyte[](data.up_to < size ? data.up_to : size);
276     data.buf = buf;
277     auto iov = uv_buf_init(cast(char*)(buf.ptr), cast(uint)buf.length);
278 
279     data.err = uv_fs_read(localLoop, req, data.handle, &iov, 1, 0, &on_read);
280     if(data.err != 0) ret(req);
281   }
282 
283   extern(C) nothrow @trusted static void on_open(uv_fs_t* req) {
284     auto result = cast(int)req.result;
285     auto data = cast(Data*)req.data;
286     if(result >= 0) {
287       data.handle = result;
288     } else {
289       data.err = result;
290       ret(req);
291       return;
292     }
293 
294     uv_fs_req_cleanup(req);
295 
296     data.err = uv_fs_fstat(localLoop, req, data.handle, &on_stat);
297     if(data.err != 0) ret(req);
298   }
299 
300   () @trusted nothrow {
301     import core.memory : GC;
302     GC.addRoot(callback.ptr);
303     auto data = castMalloc!Data;
304     data.callback = callback;
305     data.up_to = up_to;
306     data.buf = null;
307 
308     auto req = castMalloc!uv_fs_t;
309     req.data = data;
310     import std..string : toStringz;
311     data.err = uv_fs_open(localLoop, req, path.toStringz, O_RDONLY, 0, &on_open);
312     if(data.err != 0) ret(req);
313   }();
314 }
315 
316 ///
317 @system unittest {
318   import std.file : write, remove;
319   import dpromise.utils : runEventloop;
320   write("hoge.txt", "hogehogepiyopiyo");
321   scope(exit) remove("hoge.txt");
322 
323   readAsyncWithCallback("hoge.txt", size_t.max, (data, e) nothrow {
324     try {
325       assert(e is null);
326       assert(data !is null);
327 
328       assert(cast(string)data == "hogehogepiyopiyo");
329     } catch(Exception e) {}
330   });
331 
332   readAsyncWithCallback("hoge.txt", 8, (data, e) nothrow {
333     try {
334       assert(e is null);
335       assert(data !is null);
336 
337       assert(cast(string)data == "hogehoge");
338     } catch(Exception e) {}
339   });
340 
341   runEventloop();
342 }
343 
344 ///ditto
345 nothrow @safe void readAsyncWithCallback(string path, void delegate(void[], Exception) nothrow callback) {
346   readAsyncWithCallback(path, size_t.max, callback);
347 }
348 
349 ///
350 @system unittest {
351   import std.file : write, remove;
352   import dpromise.utils : runEventloop;
353 
354   write("hoge.txt", "hogehogepiyopiyo");
355   scope(exit) remove("hoge.txt");
356 
357   readAsyncWithCallback("hoge.txt", (data, e) nothrow {
358     try {
359       assert(e is null);
360       assert(data !is null);
361 
362       assert(cast(string)data == "hogehogepiyopiyo");
363     } catch(Exception e) {}
364   });
365 
366   runEventloop();
367 }
368 
369 
370 /**
371 Read file $(D path) with asynchronous IO and return data read as a promise of string(validate with $(HTTP https://dlang.org/phobos/std_utf.html#.validate, std.utf.validate))
372 
373 If an error occurred, the promise will be rejected.
374 
375 Params:
376   path = string representing file path.
377 
378 Returns: A promise of string read.
379 
380 See_Also: $(HTTP https://dlang.org/phobos/std_file.html#.readText, std.file.readText)
381 */
382 nothrow Promise!S readTextAsync(S = string)(string path) if(isSomeString!S) {
383   return promise!S((res, rej) nothrow {
384     readTextAsyncWithCallback!S(path, (s, e) {
385       e is null ? res(s) : rej(e);
386     });
387   });
388 }
389 
390 ///
391 @system unittest {
392   import std.file : write, remove;
393   import dpromise.utils : runEventloop;
394 
395   write("hoge.txt", "hogehogepiyopiyo");
396   scope(exit) remove("hoge.txt");
397 
398   readTextAsync("hoge.txt").then(
399     (s) {
400       assert(s == "hogehogepiyopiyo");
401     }
402   );
403 
404   runEventloop();
405 }
406 
407 
408 /**
409 Read file $(D path) with asynchronous IO then calls the $(D callback) function with data as string(validate with $(HTTP https://dlang.org/phobos/std_utf.html#.validate, std.utf.validate)).
410 
411 If an error occurred, the $(D callback) function will be called with the error.
412 
413 Params:
414   path = string representing file path.
415   callback = a fuction called when operation finished or an error occurred.
416 */
417 nothrow @safe void readTextAsyncWithCallback(S = string)(string path, void delegate(S, Exception) nothrow callback)
418 if(isSomeString!S)
419 in {
420   assert(callback !is null);
421 } body {
422   readAsyncWithCallback(path, (data, err) nothrow {
423     try {
424       if(err !is null) throw err;
425 
426       import std.utf : validate;
427       auto result = cast(S)data;
428       validate(result);
429       callback(result, null);
430     } catch(Exception e) {
431       callback(null, e);
432     }
433   });
434 }
435 
436 ///
437 @safe unittest {
438   import std.file : write, remove;
439   import dpromise.utils : runEventloop;
440 
441   write("hoge.txt", "hogehogepiyopiyo");
442   scope(exit) remove("hoge.txt");
443 
444   readTextAsyncWithCallback!string("hoge.txt", (s, e) nothrow {
445     assert(e is null);
446     assert(s == "hogehogepiyopiyo");
447   });
448 
449   runEventloop();
450 }