@environment-safe/file 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.mjs ADDED
@@ -0,0 +1,599 @@
1
+ /*
2
+ import { isBrowser, isJsDom } from 'browser-or-node';
3
+ import * as mod from 'module';
4
+ import * as path from 'path';
5
+ let internalRequire = null;
6
+ if(typeof require !== 'undefined') internalRequire = require;
7
+ const ensureRequire = ()=> (!internalRequire) && (internalRequire = mod.createRequire(import.meta.url));
8
+ //*/
9
+
10
+ /**
11
+ * A JSON object
12
+ * @typedef { object } JSON
13
+ */
14
+
15
+ import { isBrowser, isJsDom } from 'browser-or-node';
16
+ import { FileBuffer } from './buffer.mjs';
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+
20
+ const inputQueue = [];
21
+ const attachInputGenerator = (eventType)=>{
22
+ const handler = (event)=>{
23
+ if(inputQueue.length){
24
+ const input = inputQueue.shift();
25
+ try{
26
+ input.handler(event, input.resolve, input.reject);
27
+ }catch(ex){
28
+ inputQueue.unshift(input);
29
+ }
30
+ }
31
+ };
32
+ window.addEventListener('load', (event) => {
33
+ document.body.addEventListener(eventType, handler, false);
34
+ });
35
+ //document.body.addEventListener(eventType, handler, false);
36
+ };
37
+
38
+ if(isBrowser || isJsDom){
39
+ attachInputGenerator('mousedown');
40
+ // mousemove is cleanest, but seems unreliable
41
+ // attachInputGenerator('mousemove');
42
+ }
43
+
44
+ const wantInput = async (id, handler, cache)=>{
45
+ const promise = new Promise((resolve, reject)=>{
46
+ inputQueue.push({ resolve, reject, handler });
47
+ });
48
+ const input = await promise;
49
+ return await input;
50
+ };
51
+
52
+ const getFilePickerOptions = (name, path)=>{
53
+ let suffix = name.split('.').pop();
54
+ if(suffix.length > 6) suffix = '';
55
+ const options = {
56
+ suggestedName: name
57
+ };
58
+ if(path) options.startIn = path;
59
+ if(suffix){
60
+ const accept = {};
61
+ accept[mimesBySuffix[suffix]] = '.'+suffix;
62
+ options.types = [{
63
+ description: suffix,
64
+ accept,
65
+ }];
66
+ options.excludeAcceptAllOption = true;
67
+ }
68
+ return options;
69
+ };
70
+
71
+ const makeLocation = (path, dir)=>{
72
+ if(dir && dir[0] === '.'){
73
+ if(dir[1] === '.'){
74
+ if(dir[2] === '/' && dir[3]){
75
+ return pathJoin(File.directory.current, '..', dir.substring(3), path);
76
+ }else{
77
+ if(dir[2]){
78
+ return pathJoin(File.directory.current, '..', dir.substring(3), path);
79
+ }else{
80
+ return pathJoin(File.directory.current, '..', path);
81
+ }
82
+ }
83
+ }else{
84
+ if(dir[1] === '/'){
85
+ return pathJoin(File.directory.current, dir.substring(2), path);
86
+ }else{
87
+ if(dir[1]){
88
+ return pathJoin(File.directory.current, dir, path);
89
+ }else{
90
+ return pathJoin(File.directory.current, path);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return dir?handleCanonicalPath(dir, File.os, File.user)+ '/' + path:path;
96
+ };
97
+
98
+ export const save = async (name, dir, buffer, meta={})=>{
99
+ const location = makeLocation(name, dir);
100
+ if(isBrowser || isJsDom){
101
+ const options = getFilePickerOptions(name, dir);
102
+ const newHandle = await wantInput(location, (event, resolve, reject)=>{
103
+ try{
104
+ window.showSaveFilePicker(options).then((thisHandle)=>{
105
+ resolve(thisHandle);
106
+ }).catch((ex)=>{
107
+ reject(ex);
108
+ });
109
+ }catch(ex){
110
+ reject(ex);
111
+ }
112
+ }, meta.cache);
113
+ const writableStream = await newHandle.createWritable();
114
+ // write our file
115
+ await writableStream.write(buffer);
116
+ // close the file and write the contents to disk.
117
+ await writableStream.close();
118
+ }else{
119
+ return await new Promise((resolve, reject)=>{
120
+ fs.writeFile(location, buffer, (err)=>{
121
+ if(err) return reject(err);
122
+ resolve();
123
+ });
124
+ });
125
+ }
126
+ };
127
+
128
+ const mimesBySuffix = {
129
+ json : 'application/json',
130
+ jpg : 'image/jpeg',
131
+ jpeg : 'image/jpeg',
132
+ gif : 'image/gif',
133
+ png : 'image/png',
134
+ svg : 'image/sxg+xml',
135
+ webp : 'image/webp',
136
+ csv : 'text/csv',
137
+ tsv : 'text/tsv',
138
+ ssv : 'text/ssv',
139
+ js : 'text/javascript',
140
+ mjs : 'text/javascript',
141
+ cjs : 'text/javascript',
142
+ css : 'text/css',
143
+ };
144
+
145
+ export const pathJoin = (...parts)=>{ //returns buffer, eventually stream
146
+ if(isBrowser || isJsDom){
147
+ return parts.join('/');
148
+ }else{
149
+ return path.join.apply(path, parts);
150
+ }
151
+ };
152
+
153
+
154
+ //todo: should I remove export (no one should use this)?
155
+ export const fileBody = async (path, dir, baseDir, allowRedirect, forceReturn)=>{
156
+ try{
157
+ //let location = dir?dir+ '/' + path:path; //todo: looser handling
158
+ let location = makeLocation(path, dir);
159
+ if(canonicalLocationToPath['darwin'][dir]){
160
+ if(baseDir){
161
+ throw new Error('custom directories unsupported');
162
+ }else{
163
+ location = 'file://'+handleCanonicalPath(dir, File.os, File.user)+'/'+path;
164
+ }
165
+ }
166
+ const response = await fetch(location);
167
+ const text = await response.text();
168
+ if(!(response.ok || (allowRedirect && response.redirected) || forceReturn)){
169
+ return null;
170
+ }
171
+ return text;
172
+ }catch(ex){
173
+ //console.log(location, ex);
174
+ return null;
175
+ }
176
+ };
177
+
178
+ export const handle = async (path, dir, writable, cache={})=>{ //returns buffer, eventually stream
179
+ let suffix = path.split('.').pop();
180
+ if(suffix.length > 6) suffix = '';
181
+ const location = makeLocation(path, dir);
182
+ if(isBrowser || isJsDom){
183
+ if(cache && cache[path]) return cache[path];
184
+ const options = getFilePickerOptions(path);
185
+ try{
186
+ const response = await fileBody(path, dir);
187
+ if(response === null) throw new Error('File not found');
188
+ }catch(ex){
189
+ const newHandle = await wantInput(location, (event, resolve, reject)=>{
190
+ try{
191
+ window.showSaveFilePicker(options).then((thisHandle)=>{
192
+ resolve(thisHandle);
193
+ }).catch((ex)=>{
194
+ reject(ex);
195
+ });
196
+ }catch(ex){
197
+ reject(ex);
198
+ }
199
+ });
200
+ return newHandle;
201
+ }
202
+ const fileHandle = await wantInput(location, (event, resolve, reject)=>{
203
+ try{
204
+ window.showOpenFilePicker(options).then(([ handle ])=>{
205
+ resolve(handle);
206
+ }).catch((ex)=>{
207
+ reject(ex);
208
+ });
209
+ }catch(ex){
210
+ reject(ex);
211
+ }
212
+ }, cache);
213
+ // eslint-disable-next-line no-undef
214
+ if(cache) cache[location] = fileHandle;
215
+ // eslint-disable-next-line no-undef
216
+ return fileHandle;
217
+ }else{
218
+ // todo: impl
219
+ }
220
+ };
221
+
222
+ export const load = async (path, dir, cache)=>{ //returns buffer, eventually stream
223
+ const location = makeLocation(path, dir);
224
+ if(isBrowser || isJsDom){
225
+ try{
226
+ const response = await fetch(location);
227
+ if(!response){
228
+ return new ArrayBuffer();
229
+ }
230
+ const buffer = await response.arrayBuffer();
231
+ buffer;
232
+ return buffer;
233
+ }catch(ex){
234
+ return new ArrayBuffer();
235
+ }
236
+ }else{
237
+ return await new Promise((resolve, reject)=>{
238
+ fs.readFile(location, (err, body)=>{
239
+ if(err) return reject(err);
240
+ resolve(body);
241
+ });
242
+ });
243
+ }
244
+ };
245
+
246
+ export const exists = async (path, dir, cache, incomingHandle)=>{ //returns buffer, eventually stream
247
+ if(isBrowser || isJsDom){
248
+ if(incomingHandle){
249
+ const fileHandle = incomingHandle;
250
+ const file = await fileHandle.getFile();
251
+ const buffer = await file.arrayBuffer();
252
+ return !!buffer;
253
+ }else{
254
+ const body = await fileBody(path, dir);
255
+ return body !== null;
256
+ }
257
+ }else{
258
+ return await new Promise((resolve, reject)=>{
259
+ const location = makeLocation(path, dir);
260
+ fs.stat(location, (err, res)=>{
261
+ if(err) resolve(false);
262
+ resolve(true);
263
+ });
264
+ });
265
+ }
266
+ };
267
+
268
+ export const remove = async (path, dir, cache)=>{
269
+ if(isBrowser || isJsDom){
270
+ const fileHandle = await handle(path, dir, true, cache);
271
+ if(fileHandle.remove) fileHandle.remove(); //non-standard, but supported
272
+ }else{
273
+ // todo: impl
274
+ }
275
+ };
276
+
277
+ export const info = async (path, dir, cache)=>{
278
+ if(isBrowser || isJsDom){
279
+ // todo: impl
280
+ }else{
281
+ // todo: impl
282
+ }
283
+ };
284
+
285
+ export const list = async (path, options={})=>{
286
+ if(isBrowser || isJsDom){
287
+ // todo: impl
288
+ switch(File.agent.name){
289
+ case 'chrome': {
290
+ const page = await fileBody('', path, null, null, true);
291
+ let rows = (page && page.match( /<script>addRow\((.*)\);<\/script>/g ) ) || [];
292
+ rows = rows.map((row)=>{
293
+ return row.match( /<script>addRow\((.*)\);<\/script>/ )[1];
294
+ });
295
+ const jsonData = `[[${rows.join('], [')}]]`;
296
+ const data = JSON.parse(jsonData);
297
+ let results = data.map((meta)=>{
298
+ return {
299
+ name: meta[0],
300
+ isFile: ()=>{
301
+ return !!meta[2];
302
+ }
303
+ };
304
+ });
305
+ if(Object.keys(options).length){
306
+ if(options.files === false){
307
+ results = results.filter((file)=>{
308
+ return !file.isFile();
309
+ });
310
+ }
311
+ if(options.directories === false){
312
+ results = results.filter((file)=>{
313
+ return file.isFile();
314
+ });
315
+ }
316
+ if(!options.hidden){
317
+ results = results.filter((file)=>{
318
+ return file !== '.' && file !== '..';
319
+ });
320
+ }
321
+ }
322
+ return results.map((file)=>{
323
+ return file.name;
324
+ });
325
+ //TODO: apache fallback
326
+ //break;
327
+ }
328
+ default: throw new Error(`Usupported Browser: ${File.os}`);
329
+ }
330
+ }else{
331
+ //todo: platform safe separator
332
+ const target = path.indexOf('/') === -1?makeLocation('', path):path;
333
+ return await new Promise((resolve, reject)=>{
334
+ fs.readdir(target, { withFileTypes: true }, (err, files)=>{
335
+ if(err) return reject(err);
336
+ let results = files;
337
+ if(Object.keys(options).length){
338
+ if(options.files === false){
339
+ results = results.filter((file)=>{
340
+ return !file.isFile();
341
+ });
342
+ }
343
+ if(options.directories === false){
344
+ results = results.filter((file)=>{
345
+ return file.isFile();
346
+ });
347
+ }
348
+ if(!options.hidden){
349
+ results = results.filter((file)=>{
350
+ return file !== '.' && file !== '..';
351
+ });
352
+ }
353
+ }
354
+ resolve(results.map((file)=>{
355
+ return file.name;
356
+ }));
357
+ });
358
+ });
359
+ }
360
+ };
361
+
362
+ const internalCache = {};
363
+
364
+ export const listFiles = (path)=>{
365
+ return list(path).map((src)=>{
366
+ const url = src.indexOf('://') !== -1?src:`file://${src}`;
367
+ return new File(url);
368
+ });
369
+ };
370
+
371
+
372
+ export class File{
373
+ constructor(path, options={}){
374
+ //todo: clean this rats nest up
375
+ const location = ( (path && path[0] === '/')?`file:${path}`:path ) ||
376
+ ( (!path) && options.directory && handleCanonicalPath(options.directory, File.os, File.user) ) ||
377
+ ('/tmp/' + Math.floor( Math.random() * 10000 ));
378
+ if(options.cache === true) options.cache = internalCache;
379
+ this.options = options;
380
+ //one of: desktop, documents, downloads, music, pictures, videos
381
+ this.directory = options.directory || '.';
382
+ this.path = location;
383
+ this.buffer = new FileBuffer();
384
+ }
385
+
386
+ async save(){
387
+ await save(this.path, this.directory, this.buffer, this.options);
388
+ return this;
389
+ }
390
+
391
+ async load(){
392
+ const dir = this.path.indexOf('/') === -1?this.directory:'';
393
+ this.buffer = await load(this.path, dir, this.options);
394
+ this.buffer.cast = (type)=>{
395
+ return FileBuffer.to(type, this.buffer);
396
+ };
397
+ return this;
398
+ }
399
+
400
+ body(value){
401
+ if(value === null || value === undefined) return this.buffer;
402
+ this.buffer = FileBuffer.from(value);
403
+ this.buffer.cast = (type)=>{
404
+ return FileBuffer.to(type, this.buffer);
405
+ };
406
+ if(value) return this;
407
+ return this.buffer;
408
+ }
409
+
410
+ async info(){
411
+ return await info(this.path, this.directory);
412
+ }
413
+
414
+ async 'delete'(){
415
+ await remove(this.path, this.directory, this.options);
416
+ return this;
417
+ }
418
+
419
+ static exists(path, directory){
420
+ return exists(path, directory);
421
+ }
422
+
423
+ static list(path, options){
424
+ return list(path, options);
425
+ }
426
+ }
427
+ let user = '';
428
+ Object.defineProperty(File, 'user', {
429
+ get() {
430
+ if(isBrowser || isJsDom){
431
+ return user || 'khrome'; //todo: something real;
432
+ }else{
433
+ return user || 'khrome'; //todo: something real;
434
+ }
435
+ },
436
+ set(newValue) {
437
+ user = newValue;
438
+ },
439
+ enumerable: true,
440
+ configurable: true,
441
+ });
442
+
443
+ Object.defineProperty(File, 'os', {
444
+ get() {
445
+ if(isBrowser || isJsDom){
446
+ return 'darwin'; //todo: something real;
447
+ }else{
448
+ return 'darwin';
449
+ }
450
+ },
451
+ set(newValue) {
452
+ //do nothing
453
+ },
454
+ enumerable: true,
455
+ configurable: true,
456
+ });
457
+
458
+ const canonicalLocationToPath = {
459
+ darwin : {
460
+ 'desktop': '~/Desktop',
461
+ 'documents': '~/Documents',
462
+ 'downloads': '~/Downloads',
463
+ 'music': '~/Music',
464
+ 'pictures': '~/Pictures',
465
+ 'home': '~/Pictures',
466
+ 'videos': '~/Movies'
467
+ },
468
+ win : {},
469
+ linux : {},
470
+ };
471
+
472
+ const osToHome = {
473
+ darwin : '/Users/${user}',
474
+ win : 'C:/',
475
+ linux : '/Users/${user}',
476
+ };
477
+
478
+ /*const handlePath = (path, os, username)=>{
479
+ return path.replace('~', osToHome[os].replace('${user}', username));
480
+ };*/
481
+
482
+ const handleCanonicalPath = (name, os, username)=>{
483
+ const path = canonicalLocationToPath[os][name];
484
+ return path.replace('~', osToHome[os].replace('${user}', username));
485
+ };
486
+
487
+ File.directory = {};
488
+ Object.defineProperty(File.directory, 'current', {
489
+ get() {
490
+ if(isBrowser || isJsDom){
491
+ const base = document.getElementsByTagName('base')[0];
492
+ let basedir = null;
493
+ if(base && (basedir = base.getAttribute('href'))){
494
+ return basedir;
495
+ }else{
496
+ let path = window.location.pathname;
497
+ path = path.split('/');
498
+ path.pop(); // drop the top one
499
+ return path .join('/');
500
+ }
501
+ }else{
502
+ return process.cwd();
503
+ }
504
+ },
505
+ set(newValue) {
506
+ //do nothing
507
+ },
508
+ enumerable: true,
509
+ configurable: true,
510
+ });
511
+
512
+ Object.defineProperty(File, 'agent', {
513
+ get() {
514
+ if(isBrowser || isJsDom){
515
+ //var nVer = navigator.appVersion;
516
+ var nAgt = navigator.userAgent;
517
+ var browserName = navigator.appName;
518
+ var fullVersion = ''+parseFloat(navigator.appVersion);
519
+ var majorVersion = parseInt(navigator.appVersion,10);
520
+ var nameOffset,verOffset,ix;
521
+
522
+ // In Opera, the true version is after "Opera" or after "Version"
523
+ if ((verOffset=nAgt.indexOf('Opera'))!=-1) {
524
+ browserName = 'Opera';
525
+ fullVersion = nAgt.substring(verOffset+6);
526
+ if ((verOffset=nAgt.indexOf('Version'))!=-1)
527
+ fullVersion = nAgt.substring(verOffset+8);
528
+ }
529
+ // In MSIE, the true version is after 'MSIE' in userAgent
530
+ else if ((verOffset=nAgt.indexOf('MSIE'))!=-1) {
531
+ browserName = 'Microsoft Internet Explorer';
532
+ fullVersion = nAgt.substring(verOffset+5);
533
+ }
534
+ // In Chrome, the true version is after 'Chrome'
535
+ else if ((verOffset=nAgt.indexOf('Chrome'))!=-1) {
536
+ browserName = 'Chrome';
537
+ fullVersion = nAgt.substring(verOffset+7);
538
+ }
539
+ // In Safari, the true version is after 'Safari' or after 'Version'
540
+ else if ((verOffset=nAgt.indexOf('Safari'))!=-1) {
541
+ browserName = 'Safari';
542
+ fullVersion = nAgt.substring(verOffset+7);
543
+ if ((verOffset=nAgt.indexOf('Version'))!=-1)
544
+ fullVersion = nAgt.substring(verOffset+8);
545
+ }
546
+ // In Firefox, the true version is after 'Firefox'
547
+ else if ((verOffset=nAgt.indexOf('Firefox'))!=-1) {
548
+ browserName = 'Firefox';
549
+ fullVersion = nAgt.substring(verOffset+8);
550
+ }
551
+ // In most other browsers, 'name/version' is at the end of userAgent
552
+ else if ( (nameOffset=nAgt.lastIndexOf(' ')+1) < (verOffset=nAgt.lastIndexOf('/')) ) {
553
+ browserName = nAgt.substring(nameOffset,verOffset);
554
+ fullVersion = nAgt.substring(verOffset+1);
555
+ if (browserName.toLowerCase()==browserName.toUpperCase()) {
556
+ browserName = navigator.appName;
557
+ }
558
+ }
559
+ // trim the fullVersion string at semicolon/space if present
560
+ if ((ix=fullVersion.indexOf(';'))!=-1)
561
+ fullVersion=fullVersion.substring(0,ix);
562
+ if ((ix=fullVersion.indexOf(' '))!=-1)
563
+ fullVersion=fullVersion.substring(0,ix);
564
+
565
+ majorVersion = parseInt(''+fullVersion,10);
566
+ if (isNaN(majorVersion)) {
567
+ fullVersion = ''+parseFloat(navigator.appVersion);
568
+ majorVersion = parseInt(navigator.appVersion,10);
569
+ }
570
+ return { name: browserName.toLowerCase(), version: fullVersion, major: majorVersion };
571
+ }else{
572
+ return {};
573
+ }
574
+ },
575
+ set(newValue) {
576
+ //do nothing
577
+ },
578
+ enumerable: true,
579
+ configurable: true,
580
+ });
581
+
582
+ const directoryGet = (type)=>{
583
+ if(isBrowser || isJsDom){
584
+ return handleCanonicalPath('home', File.os, File.user);
585
+ }else{
586
+ return process.cwd();
587
+ }
588
+ };
589
+
590
+ Object.keys(canonicalLocationToPath['darwin']).forEach((key)=>{
591
+ // register all available keys
592
+ Object.defineProperty(File.directory, key, {
593
+ enumerable: true, configurable: true,
594
+ get() {
595
+ return directoryGet(key);
596
+ },
597
+ set(newValue){ }
598
+ });
599
+ });