Browse Source

Add file upload signal

Saul Shanabrook 7 years ago
parent
commit
97ec41530c
2 changed files with 112 additions and 1 deletions
  1. 55 1
      packages/filebrowser/src/model.ts
  2. 57 0
      tests/test-filebrowser/src/model.spec.ts

+ 55 - 1
packages/filebrowser/src/model.ts

@@ -57,6 +57,17 @@ export const LARGE_FILE_SIZE = 15 * 1024 * 1024;
 export const CHUNK_SIZE = 1024 * 1024;
 
 
+/**
+ * An upload progress event for a file at `path`.
+ */
+export interface IUploadModel {
+  path: string;
+  /**
+   * % uploaded [0, 1)
+   */
+  progress: number;
+}
+
 /**
  * An implementation of a file browser model.
  *
@@ -155,6 +166,20 @@ class FileBrowserModel implements IDisposable {
     return this._isDisposed;
   }
 
+  /**
+   * A signal emitted when an upload progresses.
+   */
+  get uploadChanged(): ISignal<this, IChangedArgs<IUploadModel>> {
+    return this._uploadChanged;
+  }
+
+  /**
+   * Create an iterator over the status of all in progress uploads.
+   */
+  uploads(): IIterator<IUploadModel> {
+    return new ArrayIterator(this._uploads);
+  }
+
   /**
    * Dispose of the resources held by the model.
    */
@@ -404,16 +429,42 @@ class FileBrowserModel implements IDisposable {
     }
 
     let finalModel: Contents.IModel;
+
+    let upload = {path, progress: 0};
+    this._uploadChanged.emit({name: 'start',
+      newValue: upload,
+      oldValue: null
+    });
+
     for (let start = 0; !finalModel; start += CHUNK_SIZE) {
       const end = start + CHUNK_SIZE;
       const lastChunk = end >= file.size;
       const chunk = lastChunk ? -1 : end / CHUNK_SIZE;
-      console.log(`Math.floor(start / file.size * 100)% done uploading ${path}`);
+
+      const newUpload = {path, progress: start / file.size};
+      this._uploads.splice(this._uploads.indexOf(upload));
+      this._uploads.push(newUpload);
+      this._uploadChanged.emit({
+        name: 'update',
+        newValue: newUpload,
+        oldValue: upload
+      });
+      upload = newUpload;
+
       const currentModel = await uploadInner(file.slice(start, end), chunk);
+
       if (lastChunk) {
         finalModel = currentModel;
       }
     }
+
+    this._uploads.splice(this._uploads.indexOf(upload));
+    this._uploadChanged.emit({
+      name: 'finish',
+      newValue: null,
+      oldValue: upload
+    });
+
     return finalModel;
   }
 
@@ -540,6 +591,9 @@ class FileBrowserModel implements IDisposable {
   private _driveName: string;
   private _isDisposed = false;
   private _restored = new PromiseDelegate<void>();
+  private _uploads: IUploadModel[] = [];
+  private _uploadChanged = new Signal<this, IChangedArgs<IUploadModel>>(this);
+
 }
 
 

+ 57 - 0
tests/test-filebrowser/src/model.spec.ts

@@ -26,6 +26,8 @@ import {
 import {
   acceptDialog, dismissDialog
 } from '../../utils';
+import { ISignal } from '@phosphor/signaling';
+import { IIterator } from '@phosphor/algorithm';
 
 
 describe('filebrowser/model', () => {
@@ -395,6 +397,24 @@ describe('filebrowser/model', () => {
             expect(contentsModel.content).to.be(content);
           });
         }
+        it(`should produce progress as a large file uploads`, async () => {
+          const fname = uuid() + '.txt';
+          const file = new File([new ArrayBuffer(2 * CHUNK_SIZE)], fname);
+
+          const {cleanup, values: [start, first, second, finished]} = signalToPromises(model.uploadChanged, 4);
+
+          model.upload(file);
+          expect(iteratorToList(model.uploads())).to.eql([]);
+          expect(await start).to.eql([model, {name: 'start', oldValue: null, newValue: {path: fname, progress: 0}}]);
+          expect(iteratorToList(model.uploads())).to.eql([{path: fname, progress: 0}]);
+          expect(await first).to.eql([model, {name: 'update', oldValue: {path: fname, progress: 0}, newValue: {path: fname, progress: 0}}]);
+          expect(iteratorToList(model.uploads())).to.eql([{path: fname, progress: 0}]);
+          expect(await second).to.eql([model, {name: 'update', oldValue: {path: fname, progress: 0}, newValue: {path: fname, progress: 1 / 2}}]);
+          expect(iteratorToList(model.uploads())).to.eql([{path: fname, progress: 1 / 2}]);
+          expect(await finished).to.eql([model, {name: 'finish', oldValue: {path: fname, progress: 1 / 2}, newValue: null}]);
+          expect(iteratorToList(model.uploads())).to.eql([]);
+          cleanup();
+        });
 
         after(() => {
           PageConfig.setOption('notebookVersion', prevNotebookVersion);
@@ -406,3 +426,40 @@ describe('filebrowser/model', () => {
   });
 
 });
+
+/**
+ * Creates a number of promises from a signal, which each resolve to the successive values in the signal.
+ */
+function signalToPromises<T, U>(signal: ISignal<T, U>, numberValues: number): {values: Promise<[T, U]>[], cleanup: () => void} {
+  const values: Promise<[T, U]>[] = new Array(numberValues);
+  const resolvers: Array<((value: [T, U]) => void)> = new Array(numberValues);
+
+  for (let i = 0; i < numberValues; i++) {
+    values[i] = new Promise<[T, U]>(resolve => {
+      resolvers[i] = resolve;
+    });
+  }
+
+  let current = 0;
+  function slot(sender: T, args: U) {
+    resolvers[current++]([sender, args]);
+  }
+  signal.connect(slot);
+
+  function cleanup() {
+    signal.disconnect(slot);
+  }
+  return {values, cleanup};
+}
+
+
+/**
+ * Convert an IIterator into a list.
+ */
+function iteratorToList<T>(i: IIterator<T>): T[] {
+  const a: T[] = [];
+  for (let v = i.next(); v !== undefined; v = i.next()) {
+    a.push(v);
+  }
+  return a;
+}