UNPKG

@theia/filesystem

Version:
449 lines • 19.4 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2019 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** var FileUploadService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileUploadService = exports.HTTP_UPLOAD_URL = void 0; const tslib_1 = require("tslib"); /* eslint-disable @typescript-eslint/no-explicit-any */ const inversify_1 = require("@theia/core/shared/inversify"); const uri_1 = require("@theia/core/lib/common/uri"); const cancellation_1 = require("@theia/core/lib/common/cancellation"); const promise_util_1 = require("@theia/core/lib/common/promise-util"); const message_service_1 = require("@theia/core/lib/common/message-service"); const endpoint_1 = require("@theia/core/lib/browser/endpoint"); const throttle = require("@theia/core/shared/lodash.throttle"); const file_upload_1 = require("../common/file-upload"); const async_mutex_1 = require("async-mutex"); const filesystem_preferences_1 = require("./filesystem-preferences"); const file_service_1 = require("./file-service"); const browser_1 = require("@theia/core/lib/browser"); const nls_1 = require("@theia/core/lib/common/nls"); const event_1 = require("@theia/core/lib/common/event"); exports.HTTP_UPLOAD_URL = new endpoint_1.Endpoint({ path: file_upload_1.HTTP_FILE_UPLOAD_PATH }).getRestUrl().toString(true); let FileUploadService = FileUploadService_1 = class FileUploadService { constructor() { this.onDidUploadEmitter = new event_1.Emitter(); } get onDidUpload() { return this.onDidUploadEmitter.event; } get maxConcurrentUploads() { const maxConcurrentUploads = this.fileSystemPreferences['files.maxConcurrentUploads']; return maxConcurrentUploads > 0 ? maxConcurrentUploads : Infinity; } init() { this.uploadForm = this.createUploadForm(); } createUploadForm() { const targetInput = document.createElement('input'); targetInput.type = 'text'; targetInput.spellcheck = false; targetInput.name = FileUploadService_1.TARGET; targetInput.classList.add('theia-input'); const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.classList.add('theia-input'); fileInput.name = FileUploadService_1.UPLOAD; fileInput.multiple = true; const form = document.createElement('form'); form.style.display = 'none'; form.enctype = 'multipart/form-data'; form.append(targetInput); form.append(fileInput); document.body.appendChild(form); fileInput.addEventListener('change', () => { if (this.deferredUpload && fileInput.value) { const source = new FormData(form); // clean up to allow upload to the same folder twice fileInput.value = ''; const targetUri = new uri_1.default(source.get(FileUploadService_1.TARGET)); const { resolve, reject } = this.deferredUpload; this.deferredUpload = undefined; const { onDidUpload } = this.uploadForm; this.withProgress((progress, token) => this.uploadAll(targetUri, { source, progress, token, onDidUpload }), this.uploadForm.progress).then(resolve, reject); } }); return { targetInput, fileInput }; } async upload(targetUri, params = {}) { const { source, onDidUpload, leaveInTemp } = params; if (source) { return this.withProgress((progress, token) => this.uploadAll(typeof targetUri === 'string' ? new uri_1.default(targetUri) : targetUri, { source, progress, token, leaveInTemp, onDidUpload }), params.progress); } this.deferredUpload = new promise_util_1.Deferred(); this.uploadForm.targetInput.value = String(targetUri); this.uploadForm.fileInput.click(); this.uploadForm.progress = params.progress; this.uploadForm.onDidUpload = params.onDidUpload; return this.deferredUpload.promise; } getUploadUrl() { return exports.HTTP_UPLOAD_URL; } async uploadAll(targetUri, params) { const responses = []; const status = new Map(); const result = { uploaded: [] }; /** * When `false`: display the uploading progress. * When `true`: display the server-processing progress. */ let waitingForResponses = false; const report = throttle(() => { if (waitingForResponses) { /** Number of files being processed. */ const total = status.size; /** Number of files uploaded and processed. */ let done = 0; for (const item of status.values()) { if (item.uploaded) { done += 1; } } params.progress.report({ message: nls_1.nls.localize('theia/filesystem/processedOutOf', 'Processed {0} out of {1}', done, total), work: { total, done } }); } else { /** Total number of bytes being uploaded. */ let total = 0; /** Current number of bytes uploaded. */ let done = 0; for (const item of status.values()) { total += item.total; done += item.done; } params.progress.report({ message: nls_1.nls.localize('theia/filesystem/uploadedOutOf', 'Uploaded {0} out of {1}', result.uploaded.length, status.size), work: { total, done } }); } }, 100); const uploads = []; const uploadSemaphore = new async_mutex_1.Semaphore(this.maxConcurrentUploads); try { await this.index(targetUri, params.source, { token: params.token, progress: params.progress, accept: async (item) => { if (await this.fileService.exists(item.uri) && !await this.confirmOverwrite(item.uri)) { return; } // Track and initialize the file in the status map: status.set(item.file, { total: item.file.size, done: 0 }); report(); // Don't await here: the semaphore will organize the uploading tasks, not the async indexer. uploads.push(uploadSemaphore.runExclusive(async () => { (0, cancellation_1.checkCancelled)(params.token); const { upload, response } = this.uploadFile(item.file, item.uri, params.token, params.leaveInTemp, (total, done) => { const entry = status.get(item.file); if (entry) { entry.total = total; entry.done = done; report(); } }); function onError(error) { status.delete(item.file); throw error; } responses.push(response .then(() => { (0, cancellation_1.checkCancelled)(params.token); // Consider the file uploaded once the server sends OK back. result.uploaded.push(item.uri.toString(true)); const entry = status.get(item.file); if (entry) { entry.uploaded = true; report(); } }) .catch(onError)); // Have the queue wait for the upload only. return upload .catch(onError); })); } }); (0, cancellation_1.checkCancelled)(params.token); await Promise.all(uploads); (0, cancellation_1.checkCancelled)(params.token); waitingForResponses = true; report(); await Promise.all(responses); } catch (error) { uploadSemaphore.cancel(); if (!(0, cancellation_1.isCancelled)(error)) { this.messageService.error(nls_1.nls.localize('theia/filesystem/uploadFailed', 'An error occurred while uploading a file. {0}', error.message)); throw error; } } this.onDidUploadEmitter.fire(result.uploaded); return result; } async confirmOverwrite(fileUri) { const dialog = new browser_1.ConfirmDialog({ title: nls_1.nls.localizeByDefault('Replace'), msg: nls_1.nls.localizeByDefault("A file or folder with the name '{0}' already exists in the destination folder. Do you want to replace it?", fileUri.path.base), ok: nls_1.nls.localizeByDefault('Replace'), cancel: browser_1.Dialog.CANCEL }); return !!await dialog.open(); } uploadFile(file, targetUri, token, leaveInTemp, onProgress) { const data = new FormData(); data.set('uri', targetUri.toString(true)); data.set('file', file); if (leaveInTemp) { data.set('leaveInTemp', 'true'); } // TODO: Use Fetch API once it supports upload monitoring. const xhr = new XMLHttpRequest(); token.onCancellationRequested(() => xhr.abort()); const upload = new Promise((resolve, reject) => { this.registerEvents(xhr.upload, unregister => ({ progress: (event) => { if (event.total === event.loaded) { unregister(); resolve(); } else { onProgress(event.total, event.loaded); } }, abort: () => { unregister(); reject((0, cancellation_1.cancelled)()); }, error: () => { unregister(); reject(new Error('POST upload error')); }, // `load` fires once the response is received, not when the upload is finished. // `resolve` should be called earlier within `progress` but this is a safety catch. load: () => { unregister(); if (xhr.status === 200) { resolve(); } else { reject(new Error(`POST request failed: ${xhr.status} ${xhr.statusText}`)); } }, })); }); const response = new Promise((resolve, reject) => { this.registerEvents(xhr, unregister => ({ abort: () => { unregister(); reject((0, cancellation_1.cancelled)()); }, error: () => { unregister(); reject(new Error('POST request error')); }, load: () => { unregister(); if (xhr.status === 200) { resolve(); } else if (xhr.status === 500 && xhr.statusText !== xhr.response) { // internal error with cause message // see packages/filesystem/src/node/node-file-upload-service.ts reject(new Error(`Internal server error: ${xhr.response}`)); } else { reject(new Error(`POST request failed: ${xhr.status} ${xhr.statusText}`)); } } })); }); xhr.open('POST', this.getUploadUrl(), /* async: */ true); xhr.send(data); return { upload, response }; } /** * Utility function to attach events and get a callback to unregister those. * * You may not call `unregister` in the same tick as `register` is invoked. */ registerEvents(target, register) { const events = register(() => { for (const [event, fn] of Object.entries(events)) { target.removeEventListener(event, fn); } }); for (const [event, fn] of Object.entries(events)) { target.addEventListener(event, fn); } } async withProgress(cb, { text } = { text: nls_1.nls.localize('theia/filesystem/uploadFiles', 'Uploading Files') }) { const cancellationSource = new cancellation_1.CancellationTokenSource(); const { token } = cancellationSource; const progress = await this.messageService.showProgress({ text, options: { cancelable: true } }, () => cancellationSource.cancel()); try { return await cb(progress, token); } finally { progress.cancel(); } } async index(targetUri, source, context) { if (source instanceof FormData) { await this.indexFormData(targetUri, source, context); } else if (source instanceof DataTransfer) { await this.indexDataTransfer(targetUri, source, context); } else { await this.indexCustomDataTransfer(targetUri, source, context); } } async indexFormData(targetUri, formData, context) { for (const entry of formData.getAll(FileUploadService_1.UPLOAD)) { if (entry instanceof File) { await this.indexFile(targetUri, entry, context); } } } async indexDataTransfer(targetUri, dataTransfer, context) { (0, cancellation_1.checkCancelled)(context.token); if (dataTransfer.items) { await this.indexDataTransferItemList(targetUri, dataTransfer.items, context); } else { await this.indexFileList(targetUri, dataTransfer.files, context); } } async indexCustomDataTransfer(targetUri, dataTransfer, context) { for (const [_, item] of dataTransfer) { const fileInfo = item.asFile(); if (fileInfo) { await this.indexFile(targetUri, new File([await fileInfo.data()], fileInfo.id), context); } } } async indexFileList(targetUri, files, context) { for (let i = 0; i < files.length; i++) { const file = files[i]; if (file) { await this.indexFile(targetUri, file, context); } } } async indexFile(targetUri, file, context) { await context.accept({ uri: targetUri.resolve(file.name), file }); } async indexDataTransferItemList(targetUri, items, context) { (0, cancellation_1.checkCancelled)(context.token); const entries = []; for (let i = 0; i < items.length; i++) { const entry = items[i].webkitGetAsEntry(); entries.push(entry); } await this.indexEntries(targetUri, entries, context); } async indexEntry(targetUri, entry, context) { (0, cancellation_1.checkCancelled)(context.token); if (!entry) { return; } if (entry.isDirectory) { await this.indexDirectoryEntry(targetUri, entry, context); } else { await this.indexFileEntry(targetUri, entry, context); } } /** * Read all entries within a folder by block of 100 files or folders until the * whole folder has been read. */ async indexDirectoryEntry(targetUri, entry, context) { (0, cancellation_1.checkCancelled)(context.token); const newTargetUri = targetUri.resolve(entry.name); return new Promise(async (resolve, reject) => { const reader = entry.createReader(); const getEntries = () => reader.readEntries(async (results) => { try { if (!context.token.isCancellationRequested && results && results.length) { await this.indexEntries(newTargetUri, results, context); getEntries(); // loop to read all getEntries } else { resolve(); } } catch (e) { reject(e); } }, reject); getEntries(); }); } async indexEntries(targetUri, entries, context) { (0, cancellation_1.checkCancelled)(context.token); for (let i = 0; i < entries.length; i++) { await this.indexEntry(targetUri, entries[i], context); } } async indexFileEntry(targetUri, entry, context) { await new Promise((resolve, reject) => { try { entry.file(file => this.indexFile(targetUri, file, context).then(resolve, reject), reject); } catch (e) { reject(e); } }); } }; exports.FileUploadService = FileUploadService; FileUploadService.TARGET = 'target'; FileUploadService.UPLOAD = 'upload'; tslib_1.__decorate([ (0, inversify_1.inject)(message_service_1.MessageService), tslib_1.__metadata("design:type", message_service_1.MessageService) ], FileUploadService.prototype, "messageService", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(filesystem_preferences_1.FileSystemPreferences), tslib_1.__metadata("design:type", Object) ], FileUploadService.prototype, "fileSystemPreferences", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(file_service_1.FileService), tslib_1.__metadata("design:type", file_service_1.FileService) ], FileUploadService.prototype, "fileService", void 0); tslib_1.__decorate([ (0, inversify_1.postConstruct)(), tslib_1.__metadata("design:type", Function), tslib_1.__metadata("design:paramtypes", []), tslib_1.__metadata("design:returntype", void 0) ], FileUploadService.prototype, "init", null); exports.FileUploadService = FileUploadService = FileUploadService_1 = tslib_1.__decorate([ (0, inversify_1.injectable)() ], FileUploadService); //# sourceMappingURL=file-upload-service.js.map