DEV: Add addComposerUploadPreProcessor to plugin-api (#14222)

This new interface will be used explicitly to add upload
preprocessors in the form of uppy plugins. These will be
run for each upload in the composer (dependent on the logic
of the plugin itself), before the UppyChecksum plugin is
finally run.

Since discourse-encrypt uses the existing addComposerUploadHandler
API for essentially preprocessing an upload and not uploading it
to a different place, it will be the first plugin to use this interface,
along with the register-media-optimization-upload-processor initializer
in core.

Related https://github.com/discourse/discourse-encrypt/pull/131.
This commit is contained in:
Martin Brennan 2021-09-06 08:22:50 +10:00 committed by GitHub
parent cba8b39607
commit 9873a942e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 43 deletions

View File

@ -3,6 +3,7 @@ import {
authorizesAllExtensions,
authorizesOneOrMoreImageExtensions,
} from "discourse/lib/uploads";
import { BasePlugin } from "@uppy/core";
import { resolveAllShortUrls } from "pretty-text/upload-short-url";
import {
caretPosition,
@ -61,6 +62,23 @@ export function cleanUpComposerUploadProcessor() {
uploadProcessorActions = {};
}
let uploadPreProcessors = [];
export function addComposerUploadPreProcessor(pluginClass, optionsResolverFn) {
if (!(pluginClass.prototype instanceof BasePlugin)) {
throw new Error(
"Composer upload preprocessors must inherit from the Uppy BasePlugin class."
);
}
uploadPreProcessors.push({
pluginClass,
optionsResolverFn,
});
}
export function cleanUpComposerUploadPreProcessor() {
uploadPreProcessors = [];
}
let uploadMarkdownResolvers = [];
export function addComposerUploadMarkdownResolver(resolver) {
uploadMarkdownResolvers.push(resolver);
@ -79,6 +97,7 @@ export default Component.extend(ComposerUpload, {
uploadMarkdownResolvers,
uploadProcessorActions,
uploadProcessorQueue,
uploadPreProcessors,
uploadHandlers,
@discourseComputed("composer.requiredCategoryMissing")

View File

@ -1,5 +1,4 @@
import Composer, { SAVE_ICONS, SAVE_LABELS } from "discourse/models/composer";
import { warn } from "@ember/debug";
import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { action, computed } from "@ember/object";
import { alias, and, or, reads } from "@ember/object/computed";
@ -285,17 +284,10 @@ export default Controller.extend({
return option;
},
@discourseComputed("model.isEncrypted")
composerComponent(isEncrypted) {
@discourseComputed()
composerComponent() {
const defaultComposer = "composer-editor";
if (this.siteSettings.enable_experimental_composer_uploader) {
if (isEncrypted) {
warn(
"Uppy cannot be used for composer uploads until upload handlers are developed, falling back to composer-editor.",
{ id: "composer" }
);
return defaultComposer;
}
return "composer-editor-uppy";
}
return defaultComposer;

View File

@ -1,4 +1,8 @@
import { addComposerUploadProcessor } from "discourse/components/composer-editor";
import {
addComposerUploadPreProcessor,
addComposerUploadProcessor,
} from "discourse/components/composer-editor";
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
export default {
name: "register-media-optimization-upload-processor",
@ -6,15 +10,30 @@ export default {
initialize(container) {
let siteSettings = container.lookup("site-settings:main");
if (siteSettings.composer_media_optimization_image_enabled) {
addComposerUploadProcessor(
{ action: "optimizeJPEG" },
{
optimizeJPEG: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
}
);
if (!siteSettings.enable_experimental_composer_uploader) {
addComposerUploadProcessor(
{ action: "optimizeJPEG" },
{
optimizeJPEG: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
}
);
} else {
addComposerUploadPreProcessor(
UppyMediaOptimization,
({ isMobileDevice }) => {
return {
optimizeFn: (data, opts) =>
container
.lookup("service:media-optimization-worker")
.optimizeImage(data, opts),
runParallel: !isMobileDevice,
};
}
);
}
}
},
};

View File

@ -1,6 +1,7 @@
import ComposerEditor, {
addComposerUploadHandler,
addComposerUploadMarkdownResolver,
addComposerUploadPreProcessor,
addComposerUploadProcessor,
} from "discourse/components/composer-editor";
import {
@ -1027,6 +1028,39 @@ class PluginApi {
addComposerUploadProcessor(queueItem, actionItem);
}
/**
* Registers a pre-processor for file uploads in the form
* of an Uppy preprocessor plugin.
*
* See https://uppy.io/docs/writing-plugins/ for the Uppy
* documentation, but other examples of preprocessors in core
* can be found in UppyMediaOptimization and UppyChecksum.
*
* Useful for transforming to-be uploaded files client-side.
*
* Example:
*
* api.addComposerUploadPreProcessor(UppyMediaOptimization, ({ composerModel, composerElement, capabilities, isMobileDevice }) => {
* return {
* composerModel,
* composerElement,
* capabilities,
* isMobileDevice,
* someOption: true,
* someFn: () => {},
* };
* });
*
* @param {BasePlugin} pluginClass The uppy plugin class to use for the preprocessor.
* @param {Function} optionsResolverFn This function should return an object which is passed into the constructor
* of the uppy plugin as the options argument. The object passed to the function
* contains references to the composer model, element, the capabilities of the
* browser, and isMobileDevice.
*/
addComposerUploadPreProcessor(pluginClass, optionsResolverFn) {
addComposerUploadPreProcessor(pluginClass, optionsResolverFn);
}
/**
* Registers a function to generate Markdown after a file has been uploaded.
*

View File

@ -2,7 +2,6 @@ import Mixin from "@ember/object/mixin";
import { ajax } from "discourse/lib/ajax";
import { deepMerge } from "discourse-common/lib/object";
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
import Uppy from "@uppy/core";
import DropTarget from "@uppy/drop-target";
import XHRUpload from "@uppy/xhr-upload";
@ -228,14 +227,8 @@ export default Mixin.create({
}
});
this._setupPreprocessing();
// It is important that the UppyChecksum preprocessor is the last one to
// be added; the preprocessors are run in order and since other preprocessors
// may modify the file (e.g. the UppyMediaOptimization one), we need to
// checksum once we are sure the file data has "settled".
this._uppyInstance.use(UppyChecksum, { capabilities: this.capabilities });
this._uppyInstance.use(DropTarget, { target: this.element });
this._setupPreProcessors();
this._setupUIPlugins();
// TODO (martin) Need a more automatic way to do this for preprocessor
// plugins like UppyChecksum and UppyMediaOptimization so people don't
@ -261,20 +254,31 @@ export default Mixin.create({
}
},
_setupPreprocessing() {
Object.keys(this.uploadProcessorActions).forEach((action) => {
switch (action) {
case "optimizeJPEG":
this._uppyInstance.use(UppyMediaOptimization, {
optimizeFn: this.uploadProcessorActions[action],
runParallel: !this.site.isMobileDevice,
});
this._trackPreProcessorStatus(UppyMediaOptimization);
break;
}
_setupPreProcessors() {
this.uploadPreProcessors.forEach(({ pluginClass, optionsResolverFn }) => {
this._uppyInstance.use(
pluginClass,
optionsResolverFn({
composerModel: this.composerModel,
composerElement: this.composerElement,
capabilities: this.capabilities,
isMobileDevice: this.site.isMobileDevice,
})
);
this._trackPreProcessorStatus(pluginClass);
});
// It is important that the UppyChecksum preprocessor is the last one to
// be added; the preprocessors are run in order and since other preprocessors
// may modify the file (e.g. the UppyMediaOptimization one), we need to
// checksum once we are sure the file data has "settled".
this._uppyInstance.use(UppyChecksum, { capabilities: this.capabilities });
this._uppyInstance.on("preprocess-progress", (pluginClass, file) => {
this._debugLog(
`[${pluginClass}] processing file ${file.name} (${file.id})`
);
this._preProcessorStatus[pluginClass].activeProcessing++;
let placeholderData = this.placeholders[file.id];
placeholderData.processingPlaceholder = `[${I18n.t(
@ -292,6 +296,10 @@ export default Mixin.create({
});
this._uppyInstance.on("preprocess-complete", (pluginClass, file) => {
this._debugLog(
`[${pluginClass}] completed processing file ${file.name} (${file.id})`
);
let placeholderData = this.placeholders[file.id];
this.appEvents.trigger(
`${this.eventPrefix}:replace-text`,
@ -313,6 +321,7 @@ export default Mixin.create({
isProcessingUpload: false,
isCancellable: true,
});
this._debugLog("All upload preprocessors complete.");
this.appEvents.trigger(
`${this.eventPrefix}:uploads-preprocessing-complete`
);
@ -321,6 +330,10 @@ export default Mixin.create({
});
},
_setupUIPlugins() {
this._uppyInstance.use(DropTarget, { target: this.element });
},
_uploadFilenamePlaceholder(file) {
const filename = this._filenamePlaceholder(file);
@ -593,4 +606,11 @@ export default Mixin.create({
showUploadSelector(toolbarEvent) {
this.send("showUploadSelector", toolbarEvent);
},
_debugLog(message) {
if (this.siteSettings.enable_upload_debug_mode) {
// eslint-disable-next-line no-console
console.log(message);
}
},
});

View File

@ -17,9 +17,11 @@ export default class MediaOptimizationWorkerService extends Service {
}
stopWorker() {
this.logIfDebug("Stopping media-optimization-worker...");
this.worker.terminate();
this.worker = null;
if (this.worker) {
this.logIfDebug("Stopping media-optimization-worker...");
this.worker.terminate();
this.worker = null;
}
}
ensureAvailiableWorker() {

View File

@ -46,6 +46,7 @@ import { clearNavItems } from "discourse/models/nav-item";
import {
cleanUpComposerUploadHandler,
cleanUpComposerUploadMarkdownResolver,
cleanUpComposerUploadPreProcessor,
cleanUpComposerUploadProcessor,
} from "discourse/components/composer-editor";
import { resetLastEditNotificationClick } from "discourse/models/post-stream";
@ -288,6 +289,7 @@ export function acceptance(name, optionsOrCallback) {
cleanUpComposerUploadHandler();
cleanUpComposerUploadProcessor();
cleanUpComposerUploadMarkdownResolver();
cleanUpComposerUploadPreProcessor();
resetLastEditNotificationClick();
app._runInitializer("instanceInitializers", (initName, initializer) => {
if (initializer && initializer.teardown) {