filerepository.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module upload/filerepository
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  10. import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
  11. import Collection from '@ckeditor/ckeditor5-utils/src/collection';
  12. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  13. import log from '@ckeditor/ckeditor5-utils/src/log';
  14. import FileReader from './filereader.js';
  15. import uid from '@ckeditor/ckeditor5-utils/src/uid.js';
  16. /**
  17. * File repository plugin. A central point for managing file upload.
  18. *
  19. * To use it, first you need an upload adapter. Upload adapter's job is to handle communication with the server
  20. * (sending the file and handling server's response). You can use one of the existing plugins introducing upload adapters
  21. * (e.g. {@link module:easy-image/cloudservicesuploadadapter~CloudServicesUploadAdapter} or
  22. * {@link module:adapter-ckfinder/uploadadapter~CKFinderUploadAdapter}) or write your own one
  23. * (which boils down to setting the {@link ~FileRepository#createAdapter} factory function – see
  24. * {@link ~Adapter `Adapter` interface} documentation).
  25. *
  26. * Then, you can use {@link ~FileRepository#createLoader `createLoader()`} and the returned {@link ~FileLoader} instance to
  27. * load and upload files.
  28. *
  29. * @extends module:core/plugin~Plugin
  30. */
  31. export default class FileRepository extends Plugin {
  32. /**
  33. * @inheritDoc
  34. */
  35. static get pluginName() {
  36. return 'FileRepository';
  37. }
  38. /**
  39. * @inheritDoc
  40. */
  41. init() {
  42. /**
  43. * Collection of loaders associated with this repository.
  44. *
  45. * @member {module:utils/collection~Collection} #loaders
  46. */
  47. this.loaders = new Collection();
  48. /**
  49. * A factory function which should be defined before using `FileRepository`.
  50. *
  51. * It should return a new instance of {@link module:upload/filerepository~Adapter} that will be used to upload files.
  52. * {@link module:upload/filerepository~FileLoader} instance associated with the adapter
  53. * will be passed to that function.
  54. *
  55. * For more information and example see {@link module:upload/filerepository~Adapter}.
  56. *
  57. * @member {Function} #createAdapter
  58. */
  59. /**
  60. * Number of bytes uploaded.
  61. *
  62. * @readonly
  63. * @observable
  64. * @member {Number} #uploaded
  65. */
  66. this.set( 'uploaded', 0 );
  67. /**
  68. * Number of total bytes to upload.
  69. *
  70. * It might be different than the file size because of headers and additional data.
  71. * It contains `null` if value is not available yet, so it's better to use {@link #uploadedPercent} to monitor
  72. * the progress.
  73. *
  74. * @readonly
  75. * @observable
  76. * @member {Number|null} #uploadTotal
  77. */
  78. this.set( 'uploadTotal', null );
  79. /**
  80. * Upload progress in percents.
  81. *
  82. * @readonly
  83. * @observable
  84. * @member {Number} #uploadedPercent
  85. */
  86. this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
  87. return total ? ( uploaded / total * 100 ) : 0;
  88. } );
  89. }
  90. afterInit() {
  91. if ( !this.createAdapter ) {
  92. /**
  93. * You need to enable an upload adapter in order to be able to upload files.
  94. *
  95. * This warning shows up when {@link module:upload/filerepository~FileRepository} is being used
  96. * without {@link #createAdapter definining an upload adapter}.
  97. *
  98. * If you see this warning when using one of the {@glink builds/index CKEditor 5 Builds}
  99. * it means that you did not configure any of the upload adapters available by default in those builds.
  100. * See:
  101. *
  102. * * {@link module:core/editor/editorconfig~EditorConfig#cloudServices `config.cloudServices`} for
  103. * Easy Image with Cloud Services integration,
  104. * * {@link module:core/editor/editorconfig~EditorConfig#ckfinder `config.ckfinder`} for CKFinder
  105. * file upload integration.
  106. *
  107. * If you do not need file upload functionality at all and you use one of the builds, you can disable the built-in
  108. * upload adapters to hide this warning:
  109. *
  110. * ClassicEditor
  111. * .create( document.querySelector( '#editor' ), {
  112. * removePlugins: [ 'EasyImage', 'CKFinderUploadAdapter' ]
  113. * } )
  114. * .then( ... )
  115. * .catch( ... );
  116. *
  117. * If you wish to implement your own upload adapter refer to the {@link ~Adapter `Adapter` interface} documentation.
  118. *
  119. * @error filerepository-no-adapter
  120. */
  121. log.warn( 'filerepository-no-adapter: Upload adapter is not defined.' );
  122. return null;
  123. }
  124. }
  125. /**
  126. * Returns the loader associated with specified file.
  127. *
  128. * To get loader by id use `fileRepository.loaders.get( id )`.
  129. *
  130. * @param {File} file Native file handle.
  131. * @returns {module:upload/filerepository~FileLoader|null}
  132. */
  133. getLoader( file ) {
  134. for ( const loader of this.loaders ) {
  135. if ( loader.file == file ) {
  136. return loader;
  137. }
  138. }
  139. return null;
  140. }
  141. /**
  142. * Creates a loader instance for the given file.
  143. *
  144. * Requires {@link #createAdapter} factory to be defined.
  145. *
  146. * @param {File} file Native File object.
  147. * @returns {module:upload/filerepository~FileLoader|null}
  148. */
  149. createLoader( file ) {
  150. if ( !this.createAdapter ) {
  151. log.error( 'filerepository-no-adapter: Upload adapter is not defined.' );
  152. return null;
  153. }
  154. const loader = new FileLoader( file );
  155. loader._adapter = this.createAdapter( loader );
  156. this.loaders.add( loader );
  157. loader.on( 'change:uploaded', () => {
  158. let aggregatedUploaded = 0;
  159. for ( const loader of this.loaders ) {
  160. aggregatedUploaded += loader.uploaded;
  161. }
  162. this.uploaded = aggregatedUploaded;
  163. } );
  164. loader.on( 'change:uploadTotal', () => {
  165. let aggregatedTotal = 0;
  166. for ( const loader of this.loaders ) {
  167. if ( loader.uploadTotal ) {
  168. aggregatedTotal += loader.uploadTotal;
  169. }
  170. }
  171. this.uploadTotal = aggregatedTotal;
  172. } );
  173. return loader;
  174. }
  175. /**
  176. * Destroys the given loader.
  177. *
  178. * @param {File|module:upload/filerepository~FileLoader} fileOrLoader File associated with that loader or loader
  179. * itself.
  180. */
  181. destroyLoader( fileOrLoader ) {
  182. const loader = fileOrLoader instanceof FileLoader ? fileOrLoader : this.getLoader( fileOrLoader );
  183. loader._destroy();
  184. this.loaders.remove( loader );
  185. }
  186. }
  187. mix( FileRepository, ObservableMixin );
  188. /**
  189. * File loader class.
  190. * It is used to control the process of file reading and uploading using specified adapter.
  191. */
  192. class FileLoader {
  193. /**
  194. * Creates a new instance of `FileLoader`.
  195. *
  196. * @param {File} file A native file instance.
  197. * @param {module:upload/filerepository~Adapter} adapter
  198. */
  199. constructor( file, adapter ) {
  200. /**
  201. * Unique id of FileLoader instance.
  202. *
  203. * @readonly
  204. * @member {Number}
  205. */
  206. this.id = uid();
  207. /**
  208. * A `File` instance associated with this file loader.
  209. *
  210. * @readonly
  211. * @member {File}
  212. */
  213. this.file = file;
  214. /**
  215. * Adapter instance associated with this file loader.
  216. *
  217. * @private
  218. * @member {module:upload/filerepository~Adapter}
  219. */
  220. this._adapter = adapter;
  221. /**
  222. * FileReader used by FileLoader.
  223. *
  224. * @protected
  225. * @member {module:upload/filereader~FileReader}
  226. */
  227. this._reader = new FileReader();
  228. /**
  229. * Current status of FileLoader. It can be one of the following:
  230. *
  231. * * 'idle',
  232. * * 'reading',
  233. * * 'uploading',
  234. * * 'aborted',
  235. * * 'error'.
  236. *
  237. * When reading status can change in a following way:
  238. *
  239. * `idle` -> `reading` -> `idle`
  240. * `idle` -> `reading -> `aborted`
  241. * `idle` -> `reading -> `error`
  242. *
  243. * When uploading status can change in a following way:
  244. *
  245. * `idle` -> `uploading` -> `idle`
  246. * `idle` -> `uploading` -> `aborted`
  247. * `idle` -> `uploading` -> `error`
  248. *
  249. * @readonly
  250. * @observable
  251. * @member {String} #status
  252. */
  253. this.set( 'status', 'idle' );
  254. /**
  255. * Number of bytes uploaded.
  256. *
  257. * @readonly
  258. * @observable
  259. * @member {Number} #uploaded
  260. */
  261. this.set( 'uploaded', 0 );
  262. /**
  263. * Number of total bytes to upload.
  264. *
  265. * @readonly
  266. * @observable
  267. * @member {Number|null} #uploadTotal
  268. */
  269. this.set( 'uploadTotal', null );
  270. /**
  271. * Upload progress in percents.
  272. *
  273. * @readonly
  274. * @observable
  275. * @member {Number} #uploadedPercent
  276. */
  277. this.bind( 'uploadedPercent' ).to( this, 'uploaded', this, 'uploadTotal', ( uploaded, total ) => {
  278. return total ? ( uploaded / total * 100 ) : 0;
  279. } );
  280. /**
  281. * Response of the upload.
  282. *
  283. * @readonly
  284. * @observable
  285. * @member {Object|null} #uploadResponse
  286. */
  287. this.set( 'uploadResponse', null );
  288. }
  289. /**
  290. * Reads file using {@link module:upload/filereader~FileReader}.
  291. *
  292. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-read-wrong-status` when status
  293. * is different than `idle`.
  294. *
  295. * Example usage:
  296. *
  297. * fileLoader.read()
  298. * .then( data => { ... } )
  299. * .catch( err => {
  300. * if ( err === 'aborted' ) {
  301. * console.log( 'Reading aborted.' );
  302. * } else {
  303. * console.log( 'Reading error.', err );
  304. * }
  305. * } );
  306. *
  307. * @returns {Promise} Returns promise that will be resolved with read data. Promise will be rejected if error
  308. * occurs or if read process is aborted.
  309. */
  310. read() {
  311. if ( this.status != 'idle' ) {
  312. throw new CKEditorError( 'filerepository-read-wrong-status: You cannot call read if the status is different than idle.' );
  313. }
  314. this.status = 'reading';
  315. return this._reader.read( this.file )
  316. .then( data => {
  317. this.status = 'idle';
  318. return data;
  319. } )
  320. .catch( err => {
  321. if ( err === 'aborted' ) {
  322. this.status = 'aborted';
  323. throw 'aborted';
  324. }
  325. this.status = 'error';
  326. throw this._reader.error;
  327. } );
  328. }
  329. /**
  330. * Reads file using the provided {@link module:upload/filerepository~Adapter}.
  331. *
  332. * Throws {@link module:utils/ckeditorerror~CKEditorError CKEditorError} `filerepository-upload-wrong-status` when status
  333. * is different than `idle`.
  334. * Example usage:
  335. *
  336. * fileLoader.upload()
  337. * .then( data => { ... } )
  338. * .catch( e => {
  339. * if ( e === 'aborted' ) {
  340. * console.log( 'Uploading aborted.' );
  341. * } else {
  342. * console.log( 'Uploading error.', e );
  343. * }
  344. * } );
  345. *
  346. * @returns {Promise} Returns promise that will be resolved with response data. Promise will be rejected if error
  347. * occurs or if read process is aborted.
  348. */
  349. upload() {
  350. if ( this.status != 'idle' ) {
  351. throw new CKEditorError( 'filerepository-upload-wrong-status: You cannot call upload if the status is different than idle.' );
  352. }
  353. this.status = 'uploading';
  354. return this._adapter.upload()
  355. .then( data => {
  356. this.uploadResponse = data;
  357. this.status = 'idle';
  358. return data;
  359. } )
  360. .catch( err => {
  361. if ( this.status === 'aborted' ) {
  362. throw 'aborted';
  363. }
  364. this.status = 'error';
  365. throw err;
  366. } );
  367. }
  368. /**
  369. * Aborts loading process.
  370. */
  371. abort() {
  372. const status = this.status;
  373. this.status = 'aborted';
  374. if ( status == 'reading' ) {
  375. this._reader.abort();
  376. }
  377. if ( status == 'uploading' && this._adapter.abort ) {
  378. this._adapter.abort();
  379. }
  380. this._destroy();
  381. }
  382. /**
  383. * Performs cleanup.
  384. *
  385. * @private
  386. */
  387. _destroy() {
  388. this._reader = undefined;
  389. this._adapter = undefined;
  390. this.data = undefined;
  391. this.uploadResponse = undefined;
  392. this.file = undefined;
  393. }
  394. }
  395. mix( FileLoader, ObservableMixin );
  396. /**
  397. * Adapter interface used by FileRepository to handle file upload. Adapter is a bridge between the editor and server that
  398. * handles file uploads. It should contain logic necessary to initiate upload process and monitor its progress.
  399. *
  400. * It should implement two methods:
  401. *
  402. * * {@link module:upload/filerepository~Adapter#upload `upload()`},
  403. * * {@link module:upload/filerepository~Adapter#abort `abort()`}.
  404. *
  405. * Example adapter implementation:
  406. *
  407. * class Adapter {
  408. * constructor( loader ) {
  409. * // Save Loader instance to update upload progress.
  410. * this.loader = loader;
  411. * }
  412. *
  413. * upload() {
  414. * // Update loader's progress.
  415. * server.onUploadProgress( data => {
  416. * loader.uploadTotal = data.total;
  417. * loader.uploaded = data.uploaded;
  418. * } ):
  419. *
  420. * // Return promise that will be resolved when file is uploaded.
  421. * return server.upload( loader.file );
  422. * }
  423. *
  424. * abort() {
  425. * // Reject promise returned from upload() method.
  426. * server.abortUpload();
  427. * }
  428. * }
  429. *
  430. * Then adapter can be set to be used by {@link module:upload/filerepository~FileRepository FileRepository}:
  431. *
  432. * editor.plugins.get( 'FileRepository' ).createAdapter = function( loader ) {
  433. * return new Adapter( loader );
  434. * };
  435. *
  436. * @interface Adapter
  437. */
  438. /**
  439. * Executes the upload process.
  440. * This method should return a promise that will resolve when data will be uploaded to server. Promise should be
  441. * resolved with an object containing information about uploaded file:
  442. *
  443. * {
  444. * default: 'http://server/default-size.image.png'
  445. * }
  446. *
  447. * Additionally, other image sizes can be provided:
  448. *
  449. * {
  450. * default: 'http://server/default-size.image.png',
  451. * '160': 'http://server/size-160.image.png',
  452. * '500': 'http://server/size-500.image.png',
  453. * '1000': 'http://server/size-1000.image.png'
  454. * }
  455. *
  456. * Take a look at {@link module:upload/filerepository~Adapter example Adapter implementation} and
  457. * {@link module:upload/filerepository~FileRepository#createAdapter createAdapter method}.
  458. *
  459. * @method module:upload/filerepository~Adapter#upload
  460. * @returns {Promise} Promise that should be resolved when data is uploaded.
  461. */
  462. /**
  463. * Aborts the upload process.
  464. * After aborting it should reject promise returned from {@link #upload upload()}.
  465. *
  466. * Take a look at {@link module:upload/filerepository~Adapter example Adapter implementation} and
  467. * {@link module:upload/filerepository~FileRepository#createAdapter createAdapter method}.
  468. *
  469. * @method module:upload/filerepository~Adapter#abort
  470. */