@environment-safe/file 0.2.1 → 0.3.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/.husky/pre-commit CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env sh
2
2
  . "$(dirname -- "$0")/_/husky.sh"
3
3
 
4
- #npm run lint
4
+ npm run lint
5
5
  npm run path-test
6
6
  npm run headless-browser-path-test
7
- #npm run import-test
7
+ npm run link-local-moka
8
+ npm run import-test
8
9
  npm run headless-browser-test
9
10
  #npm run build-commonjs
10
11
  #npm run require-test
package/README.md CHANGED
@@ -67,7 +67,7 @@ Roadmap
67
67
  -------
68
68
 
69
69
  - [x] - test existing suite in mac node
70
- - [ ] - test existing suite in in chrome + server
70
+ - [x] - test existing suite in in chrome + server
71
71
  - [ ] - test existing suite in in chrome + file
72
72
  - [ ] - test existing suite in windows node
73
73
  - [ ] - test existing suite in linux node
@@ -111,3 +111,5 @@ All work is done in the .mjs files and will be transpiled on commit to commonjs
111
111
 
112
112
  If the above tests pass, then attempt a commit which will generate .d.ts files alongside the `src` files and commonjs classes in `dist`
113
113
 
114
+ In order to run the `import-test`, you must link the local `moka`, which can be done with `npm run link-local-moka` This is normally solved via dependency hoisting except in the case where you are developing on the file API which has a circular dependency with `moka`.
115
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@environment-safe/file",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "src/index.mjs",
@@ -65,6 +65,7 @@
65
65
  "shims": {
66
66
  "chai": "node_modules/chai/chai.js",
67
67
  "@environment-safe/runtime-context": "node_modules/@environment-safe/runtime-context/./src/index.mjs",
68
+ "@environment-safe/file": "./src/index.mjs",
68
69
  "browser-or-node": "node_modules/browser-or-node/src/index.js"
69
70
  }
70
71
  },
@@ -76,7 +77,7 @@
76
77
  "@environment-safe/chai": "^0.1.0",
77
78
  "@environment-safe/commonjs-builder": "^0.0.3",
78
79
  "@environment-safe/jsdoc-builder": "^0.0.2",
79
- "@open-automaton/moka": "^0.5.0",
80
+ "@open-automaton/moka": "^0.5.1",
80
81
  "babel-plugin-search-and-replace": "^1.1.1",
81
82
  "babel-plugin-transform-import-meta": "^2.2.0",
82
83
  "chai": "^4.3.7",
@@ -109,6 +110,7 @@
109
110
  "container-test": "docker build . -t environment-safe-package.json -f ./containers/test.dockerfile; docker logs --follow \"$(docker run -d environment-safe-package.json)\"",
110
111
  "build-docs": "build-jsdoc docs",
111
112
  "build-types": "build-jsdoc types",
113
+ "link-local-moka":"npm link ../../@open-automaton/moka; cd ../../@open-automaton/moka; npm link ../../@environment-safe/file",
112
114
  "add-generated-files-to-commit": "git add docs/*.md; git add src/*.d.ts; git add dist/*.cjs",
113
115
  "prepare": "husky install"
114
116
  },
package/src/buffer.mjs CHANGED
@@ -26,7 +26,7 @@ const base64ToArrayBuffer = (base64)=>{
26
26
  bytes[i] = binaryString.charCodeAt(i);
27
27
  }
28
28
  return bytes.buffer;
29
- }
29
+ };
30
30
 
31
31
  if(isBrowser || isJsDom){
32
32
  InternalBuffer = function(value, type){
@@ -36,6 +36,16 @@ if(isBrowser || isJsDom){
36
36
  };
37
37
  const enc = new TextEncoder();
38
38
  const dec = new TextDecoder(); //'utf-8'
39
+ InternalBuffer.fromDataURI = async (url)=>{
40
+ if(typeof url === 'string' && url.startsWith('data:')){
41
+ const result = await fetch(url);
42
+ const buffer = await result.arrayBuffer();
43
+ return buffer;
44
+ }else{
45
+ throw new Error('not a data uri');
46
+ }
47
+ };
48
+ InternalBuffer.fromDataURL = InternalBuffer.fromDataURI;
39
49
  InternalBuffer.from = (ob)=>{
40
50
  const type = Array.isArray(ob)?'array':(typeof ob);
41
51
  if(InternalBuffer.is(ob)){
@@ -46,7 +56,6 @@ if(isBrowser || isJsDom){
46
56
  case 'array':
47
57
  case 'string':
48
58
  return enc.encode(ob).buffer;
49
- break;
50
59
  case '':
51
60
  }
52
61
  };
@@ -66,17 +75,30 @@ if(isBrowser || isJsDom){
66
75
  result = toBinString(Uint8Array.from(buffer));
67
76
  break;
68
77
  case 'base64':
69
- result = btoa(InternalBuffer.to('binary-string', buffer));
78
+ var binary = '';
79
+ var bytes = new Uint8Array( buffer );
80
+ var len = bytes.byteLength;
81
+ for (var i = 0; i < len; i++) {
82
+ binary += String.fromCharCode( bytes[ i ] );
83
+ }
84
+ result = btoa( binary );
70
85
  break;
71
86
  }
72
87
  return result;
73
88
  };
74
89
  InternalBuffer.toString = (type, buffer)=>{
75
90
  let result = null;
76
- if(type === 'base64'){
77
- result = InternalBuffer.to(type, buffer);;
78
- }else{
79
- result = InternalBuffer.to('string', buffer);
91
+ switch(type){
92
+ case 'hex':
93
+ result = Array.prototype.map.call(
94
+ new Uint8Array(buffer),
95
+ x => ('00' + x.toString(16)).slice(-2)
96
+ ).join('').match(/[a-fA-F0-9]{2}/g).join('');
97
+ break;
98
+ case 'base64':
99
+ result = InternalBuffer.to(type, buffer);
100
+ break;
101
+ default: result = InternalBuffer.to('string', buffer);
80
102
  }
81
103
  return result;
82
104
  };
@@ -87,11 +109,12 @@ if(isBrowser || isJsDom){
87
109
  result[lcv] = fill;
88
110
  }
89
111
  }
112
+ return result;
90
113
  //todo: convert encoding to byte offset
91
114
  };
92
115
  }else{
93
116
  InternalBuffer = function(value, type){
94
- return new Buffer(value, type)
117
+ return new Buffer(value, type);
95
118
  };
96
119
  InternalBuffer.from = (ob='')=>{
97
120
  return Buffer.from(ob);
@@ -99,6 +122,16 @@ if(isBrowser || isJsDom){
99
122
  InternalBuffer.is = (buffer)=>{
100
123
  return Buffer.isBuffer(buffer);
101
124
  };
125
+ InternalBuffer.fromDataURI = async (url)=>{
126
+ if(typeof url === 'string' && url.startsWith('data:')){
127
+ const result = await fetch(url);
128
+ const buffer = await result.arrayBuffer();
129
+ return buffer;
130
+ }else{
131
+ throw new Error('not a data uri');
132
+ }
133
+ };
134
+ InternalBuffer.fromDataURL = InternalBuffer.fromDataURI;
102
135
  //InternalBuffer = Buffer;
103
136
  InternalBuffer.to = (type, buffer)=>{
104
137
  switch(type){
@@ -109,7 +142,7 @@ if(isBrowser || isJsDom){
109
142
  case 'string':
110
143
  return buffer.toString();
111
144
  case 'base64':
112
- return btoa(buffer.toString());
145
+ return buffer.toString('base64');
113
146
  case '':
114
147
 
115
148
  }
@@ -119,5 +152,18 @@ if(isBrowser || isJsDom){
119
152
  return InternalBuffer.to('string', buffer);
120
153
  };
121
154
  }
155
+ InternalBuffer.stringify = (buffer, indent=' ', lineIndent='', lineSize=80)=>{
156
+ let pos = 0;
157
+ return InternalBuffer.toString('hex', buffer).split('').map((value, index)=>{
158
+ const chars = (pos%2 === 0 && pos !== 0)?indent+value:value;
159
+ if(pos + chars.length > lineSize){
160
+ const add = lineIndent+value;
161
+ pos = add.length;
162
+ return '\n'+add;
163
+ }
164
+ pos += chars.length;
165
+ return chars;
166
+ }).join('');
167
+ };
122
168
 
123
169
  export const FileBuffer = InternalBuffer;
@@ -16,6 +16,7 @@ const ensureRequire = ()=> (!internalRequire) && (internalRequire = mod.createRe
16
16
  // and we now have 4 an obvious indicator that we went wrong? (win, unix, file, known dirs)
17
17
 
18
18
  import { FileBuffer } from './buffer.mjs';
19
+ import { File } from './index.mjs';
19
20
  import * as fs from 'fs';
20
21
  import {
21
22
  isClient // is running a client
@@ -331,7 +332,9 @@ export const serverFile = {
331
332
  const url = parsed.toUrl('native');
332
333
  return await new Promise((resolve, reject)=>{
333
334
  fs.readFile(url, (err, buffer)=>{
334
- if(err) return reject(err);
335
+ if(err){
336
+ reject(new Error(`File not found('${path}')`));
337
+ }
335
338
  resolve(buffer);
336
339
  });
337
340
  });
@@ -342,15 +345,17 @@ export const serverFile = {
342
345
  return await new Promise((resolve, reject)=>{
343
346
  fs.writeFile(url, buffer, (err)=>{
344
347
  if(err) return reject(err);
345
- if(globalThis.handleWrite) globalThis.handleWrite({
346
- path,
347
- url,
348
- buffer,
349
- text: ()=>{
350
- return FileBuffer.toString('string', buffer)
351
- },
352
- arrayBuffer: ()=> buffer,
353
- });
348
+ if(globalThis.handleWrite){
349
+ globalThis.handleWrite({
350
+ path,
351
+ url,
352
+ buffer,
353
+ text: ()=>{
354
+ return FileBuffer.toString('string', buffer);
355
+ },
356
+ arrayBuffer: ()=> buffer,
357
+ });
358
+ }
354
359
  resolve();
355
360
  });
356
361
  });
@@ -431,18 +436,19 @@ export const remote = {
431
436
  },
432
437
  read: async (path, options={})=>{
433
438
  const response = await fetch(path);
439
+ if(response.status === 404) throw new Error(`File not found('${path}')`);
434
440
  return await response.arrayBuffer();
435
441
  },
436
442
  write: async (path, buffer, options={})=>{
437
443
  return await new Promise((resolve, reject)=>{
438
444
  try{
439
445
  var element = document.createElement('a');
440
- const type = options.mimeType || 'text/plain';
441
- const format = options.format || 'charset=utf-8';
446
+ const type = options.mimeType || File.deriveMIMEType(buffer);
447
+ const format = options.format || File.deriveFormat(type);
442
448
  const representation = (
443
449
  type === 'text/plain'?
444
- FileBuffer.toString('text', buffer):
445
- FileBuffer.toString('base64', buffer)
450
+ FileBuffer.toString('text', buffer):
451
+ FileBuffer.toString('base64', buffer)
446
452
  );
447
453
  const dataURI = `data:${type};${format},${representation}`;
448
454
  element.setAttribute('href', dataURI);
package/src/index.mjs CHANGED
@@ -131,33 +131,50 @@ const internalCache = {};
131
131
 
132
132
  const mimeTypes = [
133
133
  {
134
- check: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
135
- mime: 'image/png'
134
+ check: [0x89, 0x50, 0x4e, 0x47],
135
+ mime: 'image/png',
136
+ types: ['png']
136
137
  },
137
138
  {
138
139
  check: [0xff, 0xd8, 0xff],
139
- mime: 'image/jpeg'
140
+ mime: 'image/jpeg',
141
+ types: ['jpg', 'jpeg']
140
142
  },
141
143
  {
142
144
  check: [0x47, 0x49, 0x46, 0x38],
143
- mime: 'image/gif'
145
+ mime: 'image/gif',
146
+ types: ['gif']
144
147
  }
145
148
  ];
149
+ const textMimeTypes = [
150
+ 'text/plain'
151
+ ];
146
152
 
147
153
  const checkOne = (headers)=>{
148
- return (buffers, options = { offset: 0 }) =>
149
- headers.every(
150
- (header, index) => header === buffers[options.offset + index]
151
- );
154
+ return (buffers, options = { offset: 0 }) =>{
155
+ const array = new Uint8Array(buffers);
156
+ return headers.reduce((agg, value, index)=>{
157
+ return agg && value === array[options.offset + index];
158
+ }, true);
159
+ };
152
160
  };
153
161
 
154
162
  const checks = {};
155
163
 
164
+ /*const mimeFromType = (path)=>{
165
+ const fileType = path.split('.').pop().toLowerCase();
166
+ const result = mimeTypes.reduce((agg, type)=>{
167
+ return agg || (type.types.indexOf(fileType) !== -1 && fileType)
168
+ }, false);
169
+ return result;
170
+ };*/
171
+
156
172
  const mimeFromBuffer = (buffer)=>{
157
- return mimeTypes.reduce((agg, type)=>{
173
+ const result = mimeTypes.reduce((agg, type)=>{
158
174
  if(!checks[type.check]) checks[type.check] = checkOne(type.check);
159
- return agg || (checks[type.check](buffer) && type.mime)
175
+ return agg || (checks[type.check](buffer) && type.mime);
160
176
  }, false);
177
+ return result;
161
178
  };
162
179
 
163
180
  export class File{
@@ -170,7 +187,11 @@ export class File{
170
187
  this.options = options;
171
188
  //one of: desktop, documents, downloads, music, pictures, videos
172
189
  this.directory = options.directory || '.';
173
- this.path = location;
190
+ if(location.startsWith('data:')){
191
+ this.dataURI = location;
192
+ }else{
193
+ this.path = location;
194
+ }
174
195
  this.setBuffer(FileBuffer.from(''));
175
196
  }
176
197
 
@@ -188,8 +209,14 @@ export class File{
188
209
  }
189
210
 
190
211
  async load(){
191
- this.setBuffer(await read(this.path, this.options));
192
- return this;
212
+ if(this.path){
213
+ const input = await read(this.path, this.options);
214
+ this.setBuffer(input);
215
+ return this;
216
+ }
217
+ if(this.dataURI){
218
+ this.setBuffer(await FileBuffer.fromDataURI(this.dataURI));
219
+ }
193
220
  }
194
221
 
195
222
  body(value){
@@ -207,13 +234,21 @@ export class File{
207
234
  return this.derivedMIMEType;
208
235
  }
209
236
 
237
+ isText(){
238
+ return textMimeTypes.indexOf(this.mimeType()) !== -1;
239
+ }
240
+
210
241
  format(){
211
242
  if(!this.derivedMIMEType) this.mimeType();
212
243
  return File.deriveFormat(this.derivedMIMEType);
213
244
  }
214
245
 
215
246
  toDataURL(){
216
- return `data:${this.mimeType()};${this.format()},${FileBuffer.toString('base64', this.body())}`
247
+ if(this.isText()){
248
+ return `data:${this.mimeType()};${this.format()},${FileBuffer.toString('string', this.body())}`;
249
+ }else{
250
+ return `data:${this.mimeType()};${this.format()},${FileBuffer.toString('base64', this.body())}`;
251
+ }
217
252
  }
218
253
 
219
254
  async info(){
@@ -246,6 +281,20 @@ export class File{
246
281
  return await exists(path, directory);
247
282
  }
248
283
 
284
+ static async similarity(fileA, fileB, strict){
285
+ //*
286
+ if(strict && fileA.buffer.byteLength !== fileB.buffer.byteLength){
287
+ throw new Error(`File lengths do not match! (${fileA.buffer.byteLength} != ${fileB.buffer.byteLength})`);
288
+ } //*/
289
+ const size = fileA.buffer.byteLength;
290
+ let total = 0;
291
+ let lcv=0;
292
+ for(lcv=0; lcv<size; lcv++){
293
+ if(fileA[lcv] === fileB[lcv]) total++;
294
+ }
295
+ return total/size;
296
+ }
297
+
249
298
  static async list(path, options){
250
299
  return await list(path, options);
251
300
  }
package/test/aso.png ADDED
Binary file
package/test/test.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  /* global describe:false */
2
- import { isServer } from '@environment-safe/runtime-context';
3
2
  import { chai } from '@environment-safe/chai';
4
- import { it, configure, interactive } from '@open-automaton/moka';
3
+ import { it, configure, interactive, isInteractive } from '@open-automaton/moka';
5
4
  import { File, Path, Download } from '../src/index.mjs';
6
5
  const should = chai.should();
7
6
 
@@ -18,9 +17,6 @@ describe('@environment-safe/file', ()=>{
18
17
  const fileRelativeSaveName = Path.join(Path.location('downloads'), 'index.mjs');
19
18
  const missingFileRelativeName = Path.join('..', 'src', 'unknown.mjs');
20
19
  const download = new Download();
21
- if(isServer){
22
-
23
- }
24
20
  configure({
25
21
  dialog : async (context, actions)=> await actions.confirm(), // OK everything,
26
22
  wantsInput : async (context, actions)=> await actions.click('#mocha'), // click everything
@@ -93,11 +89,15 @@ describe('@environment-safe/file', ()=>{
93
89
  await file.load();
94
90
  file.path = fileRelativeSaveName; //change path
95
91
  file.body('foo!'); //replace source
96
- const anticipatedDownload = download.expect();
97
- await file.save();
98
- const result = await anticipatedDownload;
99
- const downloadedText = await result.text();
100
- downloadedText.should.equal('foo!');
92
+ if(!isInteractive){
93
+ // if the test is interactive, automated file hooks won't work
94
+ // so test without a download confirm
95
+ const anticipatedDownload = download.expect();
96
+ await file.save();
97
+ const result = await anticipatedDownload;
98
+ const downloadedText = await result.text();
99
+ downloadedText.should.equal('foo!');
100
+ }
101
101
  });
102
102
 
103
103
  it('loads an implicit, relative URL', async function(){
@@ -116,6 +116,53 @@ describe('@environment-safe/file', ()=>{
116
116
  await file.load();
117
117
  file.body().cast('string').length.should.be.above(1);
118
118
  });
119
+
120
+ it('loads itself as data when text', async function(){
121
+ const file = new File(Path.join(
122
+ '../node_modules/@environment-safe/chai',
123
+ 'README.md'
124
+ ));
125
+ await file.load();
126
+ const uri = file.toDataURL();
127
+ const copy = new File(uri);
128
+ await copy.load();
129
+ const similarity = await File.similarity(file, copy);
130
+ should.exist(similarity);
131
+ similarity.should.equal(1);
132
+ });
133
+
134
+ it('loads itself as data when binary', async function(){
135
+ const file = new File('./aso.png');
136
+ await file.load();
137
+ const uri = file.toDataURL();
138
+ const copy = new File(uri);
139
+ await copy.load();
140
+ const similarity = await File.similarity(file, copy);
141
+ should.exist(similarity);
142
+ similarity.should.equal(1);
143
+ });
144
+
145
+ it('can duplicate by copying buffer', async function(){
146
+ const file = new File('./aso.png');
147
+ await file.load();
148
+ const copy = new File();
149
+ copy.body(file.buffer);
150
+ const similarity = await File.similarity(file, copy, true);
151
+ should.exist(similarity);
152
+ similarity.should.equal(1);
153
+ });
154
+
155
+ it('fails to load a non-existent file', async function(){
156
+
157
+ try{
158
+ const file = new File('./woo.png');
159
+ await file.load();
160
+ }catch(ex){
161
+ ex.message.should.equal('File not found(\'./woo.png\')');
162
+ return;
163
+ }
164
+ should.not.exist(true, 'file did not error on load');
165
+ });
119
166
  });
120
167
  });
121
168