web.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. import { WebPlugin, buildRequestInit } from '@capacitor/core';
  2. import { Encoding } from './definitions';
  3. function resolve(path) {
  4. const posix = path.split('/').filter((item) => item !== '.');
  5. const newPosix = [];
  6. posix.forEach((item) => {
  7. if (item === '..' && newPosix.length > 0 && newPosix[newPosix.length - 1] !== '..') {
  8. newPosix.pop();
  9. }
  10. else {
  11. newPosix.push(item);
  12. }
  13. });
  14. return newPosix.join('/');
  15. }
  16. function isPathParent(parent, children) {
  17. parent = resolve(parent);
  18. children = resolve(children);
  19. const pathsA = parent.split('/');
  20. const pathsB = children.split('/');
  21. return parent !== children && pathsA.every((value, index) => value === pathsB[index]);
  22. }
  23. export class FilesystemWeb extends WebPlugin {
  24. constructor() {
  25. super(...arguments);
  26. this.DB_VERSION = 1;
  27. this.DB_NAME = 'Disc';
  28. this._writeCmds = ['add', 'put', 'delete'];
  29. /**
  30. * Function that performs a http request to a server and downloads the file to the specified destination
  31. *
  32. * @deprecated Use the @capacitor/file-transfer plugin instead.
  33. * @param options the options for the download operation
  34. * @returns a promise that resolves with the download file result
  35. */
  36. this.downloadFile = async (options) => {
  37. var _a, _b;
  38. const requestInit = buildRequestInit(options, options.webFetchExtra);
  39. const response = await fetch(options.url, requestInit);
  40. let blob;
  41. if (!options.progress)
  42. blob = await response.blob();
  43. else if (!(response === null || response === void 0 ? void 0 : response.body))
  44. blob = new Blob();
  45. else {
  46. const reader = response.body.getReader();
  47. let bytes = 0;
  48. const chunks = [];
  49. const contentType = response.headers.get('content-type');
  50. const contentLength = parseInt(response.headers.get('content-length') || '0', 10);
  51. while (true) {
  52. const { done, value } = await reader.read();
  53. if (done)
  54. break;
  55. chunks.push(value);
  56. bytes += (value === null || value === void 0 ? void 0 : value.length) || 0;
  57. const status = {
  58. url: options.url,
  59. bytes,
  60. contentLength,
  61. };
  62. this.notifyListeners('progress', status);
  63. }
  64. const allChunks = new Uint8Array(bytes);
  65. let position = 0;
  66. for (const chunk of chunks) {
  67. if (typeof chunk === 'undefined')
  68. continue;
  69. allChunks.set(chunk, position);
  70. position += chunk.length;
  71. }
  72. blob = new Blob([allChunks.buffer], { type: contentType || undefined });
  73. }
  74. const result = await this.writeFile({
  75. path: options.path,
  76. directory: (_a = options.directory) !== null && _a !== void 0 ? _a : undefined,
  77. recursive: (_b = options.recursive) !== null && _b !== void 0 ? _b : false,
  78. data: blob,
  79. });
  80. return { path: result.uri, blob };
  81. };
  82. }
  83. readFileInChunks(_options, _callback) {
  84. throw this.unavailable('Method not implemented.');
  85. }
  86. async initDb() {
  87. if (this._db !== undefined) {
  88. return this._db;
  89. }
  90. if (!('indexedDB' in window)) {
  91. throw this.unavailable("This browser doesn't support IndexedDB");
  92. }
  93. return new Promise((resolve, reject) => {
  94. const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
  95. request.onupgradeneeded = FilesystemWeb.doUpgrade;
  96. request.onsuccess = () => {
  97. this._db = request.result;
  98. resolve(request.result);
  99. };
  100. request.onerror = () => reject(request.error);
  101. request.onblocked = () => {
  102. console.warn('db blocked');
  103. };
  104. });
  105. }
  106. static doUpgrade(event) {
  107. const eventTarget = event.target;
  108. const db = eventTarget.result;
  109. switch (event.oldVersion) {
  110. case 0:
  111. case 1:
  112. default: {
  113. if (db.objectStoreNames.contains('FileStorage')) {
  114. db.deleteObjectStore('FileStorage');
  115. }
  116. const store = db.createObjectStore('FileStorage', { keyPath: 'path' });
  117. store.createIndex('by_folder', 'folder');
  118. }
  119. }
  120. }
  121. async dbRequest(cmd, args) {
  122. const readFlag = this._writeCmds.indexOf(cmd) !== -1 ? 'readwrite' : 'readonly';
  123. return this.initDb().then((conn) => {
  124. return new Promise((resolve, reject) => {
  125. const tx = conn.transaction(['FileStorage'], readFlag);
  126. const store = tx.objectStore('FileStorage');
  127. const req = store[cmd](...args);
  128. req.onsuccess = () => resolve(req.result);
  129. req.onerror = () => reject(req.error);
  130. });
  131. });
  132. }
  133. async dbIndexRequest(indexName, cmd, args) {
  134. const readFlag = this._writeCmds.indexOf(cmd) !== -1 ? 'readwrite' : 'readonly';
  135. return this.initDb().then((conn) => {
  136. return new Promise((resolve, reject) => {
  137. const tx = conn.transaction(['FileStorage'], readFlag);
  138. const store = tx.objectStore('FileStorage');
  139. const index = store.index(indexName);
  140. const req = index[cmd](...args);
  141. req.onsuccess = () => resolve(req.result);
  142. req.onerror = () => reject(req.error);
  143. });
  144. });
  145. }
  146. getPath(directory, uriPath) {
  147. const cleanedUriPath = uriPath !== undefined ? uriPath.replace(/^[/]+|[/]+$/g, '') : '';
  148. let fsPath = '';
  149. if (directory !== undefined)
  150. fsPath += '/' + directory;
  151. if (uriPath !== '')
  152. fsPath += '/' + cleanedUriPath;
  153. return fsPath;
  154. }
  155. async clear() {
  156. const conn = await this.initDb();
  157. const tx = conn.transaction(['FileStorage'], 'readwrite');
  158. const store = tx.objectStore('FileStorage');
  159. store.clear();
  160. }
  161. /**
  162. * Read a file from disk
  163. * @param options options for the file read
  164. * @return a promise that resolves with the read file data result
  165. */
  166. async readFile(options) {
  167. const path = this.getPath(options.directory, options.path);
  168. // const encoding = options.encoding;
  169. const entry = (await this.dbRequest('get', [path]));
  170. if (entry === undefined)
  171. throw Error('File does not exist.');
  172. return { data: entry.content ? entry.content : '' };
  173. }
  174. /**
  175. * Write a file to disk in the specified location on device
  176. * @param options options for the file write
  177. * @return a promise that resolves with the file write result
  178. */
  179. async writeFile(options) {
  180. const path = this.getPath(options.directory, options.path);
  181. let data = options.data;
  182. const encoding = options.encoding;
  183. const doRecursive = options.recursive;
  184. const occupiedEntry = (await this.dbRequest('get', [path]));
  185. if (occupiedEntry && occupiedEntry.type === 'directory')
  186. throw Error('The supplied path is a directory.');
  187. const parentPath = path.substr(0, path.lastIndexOf('/'));
  188. const parentEntry = (await this.dbRequest('get', [parentPath]));
  189. if (parentEntry === undefined) {
  190. const subDirIndex = parentPath.indexOf('/', 1);
  191. if (subDirIndex !== -1) {
  192. const parentArgPath = parentPath.substr(subDirIndex);
  193. await this.mkdir({
  194. path: parentArgPath,
  195. directory: options.directory,
  196. recursive: doRecursive,
  197. });
  198. }
  199. }
  200. if (!encoding && !(data instanceof Blob)) {
  201. data = data.indexOf(',') >= 0 ? data.split(',')[1] : data;
  202. if (!this.isBase64String(data))
  203. throw Error('The supplied data is not valid base64 content.');
  204. }
  205. const now = Date.now();
  206. const pathObj = {
  207. path: path,
  208. folder: parentPath,
  209. type: 'file',
  210. size: data instanceof Blob ? data.size : data.length,
  211. ctime: now,
  212. mtime: now,
  213. content: data,
  214. };
  215. await this.dbRequest('put', [pathObj]);
  216. return {
  217. uri: pathObj.path,
  218. };
  219. }
  220. /**
  221. * Append to a file on disk in the specified location on device
  222. * @param options options for the file append
  223. * @return a promise that resolves with the file write result
  224. */
  225. async appendFile(options) {
  226. const path = this.getPath(options.directory, options.path);
  227. let data = options.data;
  228. const encoding = options.encoding;
  229. const parentPath = path.substr(0, path.lastIndexOf('/'));
  230. const now = Date.now();
  231. let ctime = now;
  232. const occupiedEntry = (await this.dbRequest('get', [path]));
  233. if (occupiedEntry && occupiedEntry.type === 'directory')
  234. throw Error('The supplied path is a directory.');
  235. const parentEntry = (await this.dbRequest('get', [parentPath]));
  236. if (parentEntry === undefined) {
  237. const subDirIndex = parentPath.indexOf('/', 1);
  238. if (subDirIndex !== -1) {
  239. const parentArgPath = parentPath.substr(subDirIndex);
  240. await this.mkdir({
  241. path: parentArgPath,
  242. directory: options.directory,
  243. recursive: true,
  244. });
  245. }
  246. }
  247. if (!encoding && !this.isBase64String(data))
  248. throw Error('The supplied data is not valid base64 content.');
  249. if (occupiedEntry !== undefined) {
  250. if (occupiedEntry.content instanceof Blob) {
  251. throw Error('The occupied entry contains a Blob object which cannot be appended to.');
  252. }
  253. if (occupiedEntry.content !== undefined && !encoding) {
  254. data = btoa(atob(occupiedEntry.content) + atob(data));
  255. }
  256. else {
  257. data = occupiedEntry.content + data;
  258. }
  259. ctime = occupiedEntry.ctime;
  260. }
  261. const pathObj = {
  262. path: path,
  263. folder: parentPath,
  264. type: 'file',
  265. size: data.length,
  266. ctime: ctime,
  267. mtime: now,
  268. content: data,
  269. };
  270. await this.dbRequest('put', [pathObj]);
  271. }
  272. /**
  273. * Delete a file from disk
  274. * @param options options for the file delete
  275. * @return a promise that resolves with the deleted file data result
  276. */
  277. async deleteFile(options) {
  278. const path = this.getPath(options.directory, options.path);
  279. const entry = (await this.dbRequest('get', [path]));
  280. if (entry === undefined)
  281. throw Error('File does not exist.');
  282. const entries = await this.dbIndexRequest('by_folder', 'getAllKeys', [IDBKeyRange.only(path)]);
  283. if (entries.length !== 0)
  284. throw Error('Folder is not empty.');
  285. await this.dbRequest('delete', [path]);
  286. }
  287. /**
  288. * Create a directory.
  289. * @param options options for the mkdir
  290. * @return a promise that resolves with the mkdir result
  291. */
  292. async mkdir(options) {
  293. const path = this.getPath(options.directory, options.path);
  294. const doRecursive = options.recursive;
  295. const parentPath = path.substr(0, path.lastIndexOf('/'));
  296. const depth = (path.match(/\//g) || []).length;
  297. const parentEntry = (await this.dbRequest('get', [parentPath]));
  298. const occupiedEntry = (await this.dbRequest('get', [path]));
  299. if (depth === 1)
  300. throw Error('Cannot create Root directory');
  301. if (occupiedEntry !== undefined)
  302. throw Error('Current directory does already exist.');
  303. if (!doRecursive && depth !== 2 && parentEntry === undefined)
  304. throw Error('Parent directory must exist');
  305. if (doRecursive && depth !== 2 && parentEntry === undefined) {
  306. const parentArgPath = parentPath.substr(parentPath.indexOf('/', 1));
  307. await this.mkdir({
  308. path: parentArgPath,
  309. directory: options.directory,
  310. recursive: doRecursive,
  311. });
  312. }
  313. const now = Date.now();
  314. const pathObj = {
  315. path: path,
  316. folder: parentPath,
  317. type: 'directory',
  318. size: 0,
  319. ctime: now,
  320. mtime: now,
  321. };
  322. await this.dbRequest('put', [pathObj]);
  323. }
  324. /**
  325. * Remove a directory
  326. * @param options the options for the directory remove
  327. */
  328. async rmdir(options) {
  329. const { path, directory, recursive } = options;
  330. const fullPath = this.getPath(directory, path);
  331. const entry = (await this.dbRequest('get', [fullPath]));
  332. if (entry === undefined)
  333. throw Error('Folder does not exist.');
  334. if (entry.type !== 'directory')
  335. throw Error('Requested path is not a directory');
  336. const readDirResult = await this.readdir({ path, directory });
  337. if (readDirResult.files.length !== 0 && !recursive)
  338. throw Error('Folder is not empty');
  339. for (const entry of readDirResult.files) {
  340. const entryPath = `${path}/${entry.name}`;
  341. const entryObj = await this.stat({ path: entryPath, directory });
  342. if (entryObj.type === 'file') {
  343. await this.deleteFile({ path: entryPath, directory });
  344. }
  345. else {
  346. await this.rmdir({ path: entryPath, directory, recursive });
  347. }
  348. }
  349. await this.dbRequest('delete', [fullPath]);
  350. }
  351. /**
  352. * Return a list of files from the directory (not recursive)
  353. * @param options the options for the readdir operation
  354. * @return a promise that resolves with the readdir directory listing result
  355. */
  356. async readdir(options) {
  357. const path = this.getPath(options.directory, options.path);
  358. const entry = (await this.dbRequest('get', [path]));
  359. if (options.path !== '' && entry === undefined)
  360. throw Error('Folder does not exist.');
  361. const entries = await this.dbIndexRequest('by_folder', 'getAllKeys', [IDBKeyRange.only(path)]);
  362. const files = await Promise.all(entries.map(async (e) => {
  363. let subEntry = (await this.dbRequest('get', [e]));
  364. if (subEntry === undefined) {
  365. subEntry = (await this.dbRequest('get', [e + '/']));
  366. }
  367. return {
  368. name: e.substring(path.length + 1),
  369. type: subEntry.type,
  370. size: subEntry.size,
  371. ctime: subEntry.ctime,
  372. mtime: subEntry.mtime,
  373. uri: subEntry.path,
  374. };
  375. }));
  376. return { files: files };
  377. }
  378. /**
  379. * Return full File URI for a path and directory
  380. * @param options the options for the stat operation
  381. * @return a promise that resolves with the file stat result
  382. */
  383. async getUri(options) {
  384. const path = this.getPath(options.directory, options.path);
  385. let entry = (await this.dbRequest('get', [path]));
  386. if (entry === undefined) {
  387. entry = (await this.dbRequest('get', [path + '/']));
  388. }
  389. return {
  390. uri: (entry === null || entry === void 0 ? void 0 : entry.path) || path,
  391. };
  392. }
  393. /**
  394. * Return data about a file
  395. * @param options the options for the stat operation
  396. * @return a promise that resolves with the file stat result
  397. */
  398. async stat(options) {
  399. const path = this.getPath(options.directory, options.path);
  400. let entry = (await this.dbRequest('get', [path]));
  401. if (entry === undefined) {
  402. entry = (await this.dbRequest('get', [path + '/']));
  403. }
  404. if (entry === undefined)
  405. throw Error('Entry does not exist.');
  406. return {
  407. name: entry.path.substring(path.length + 1),
  408. type: entry.type,
  409. size: entry.size,
  410. ctime: entry.ctime,
  411. mtime: entry.mtime,
  412. uri: entry.path,
  413. };
  414. }
  415. /**
  416. * Rename a file or directory
  417. * @param options the options for the rename operation
  418. * @return a promise that resolves with the rename result
  419. */
  420. async rename(options) {
  421. await this._copy(options, true);
  422. return;
  423. }
  424. /**
  425. * Copy a file or directory
  426. * @param options the options for the copy operation
  427. * @return a promise that resolves with the copy result
  428. */
  429. async copy(options) {
  430. return this._copy(options, false);
  431. }
  432. async requestPermissions() {
  433. return { publicStorage: 'granted' };
  434. }
  435. async checkPermissions() {
  436. return { publicStorage: 'granted' };
  437. }
  438. /**
  439. * Function that can perform a copy or a rename
  440. * @param options the options for the rename operation
  441. * @param doRename whether to perform a rename or copy operation
  442. * @return a promise that resolves with the result
  443. */
  444. async _copy(options, doRename = false) {
  445. let { toDirectory } = options;
  446. const { to, from, directory: fromDirectory } = options;
  447. if (!to || !from) {
  448. throw Error('Both to and from must be provided');
  449. }
  450. // If no "to" directory is provided, use the "from" directory
  451. if (!toDirectory) {
  452. toDirectory = fromDirectory;
  453. }
  454. const fromPath = this.getPath(fromDirectory, from);
  455. const toPath = this.getPath(toDirectory, to);
  456. // Test that the "to" and "from" locations are different
  457. if (fromPath === toPath) {
  458. return {
  459. uri: toPath,
  460. };
  461. }
  462. if (isPathParent(fromPath, toPath)) {
  463. throw Error('To path cannot contain the from path');
  464. }
  465. // Check the state of the "to" location
  466. let toObj;
  467. try {
  468. toObj = await this.stat({
  469. path: to,
  470. directory: toDirectory,
  471. });
  472. }
  473. catch (e) {
  474. // To location does not exist, ensure the directory containing "to" location exists and is a directory
  475. const toPathComponents = to.split('/');
  476. toPathComponents.pop();
  477. const toPath = toPathComponents.join('/');
  478. // Check the containing directory of the "to" location exists
  479. if (toPathComponents.length > 0) {
  480. const toParentDirectory = await this.stat({
  481. path: toPath,
  482. directory: toDirectory,
  483. });
  484. if (toParentDirectory.type !== 'directory') {
  485. throw new Error('Parent directory of the to path is a file');
  486. }
  487. }
  488. }
  489. // Cannot overwrite a directory
  490. if (toObj && toObj.type === 'directory') {
  491. throw new Error('Cannot overwrite a directory with a file');
  492. }
  493. // Ensure the "from" object exists
  494. const fromObj = await this.stat({
  495. path: from,
  496. directory: fromDirectory,
  497. });
  498. // Set the mtime/ctime of the supplied path
  499. const updateTime = async (path, ctime, mtime) => {
  500. const fullPath = this.getPath(toDirectory, path);
  501. const entry = (await this.dbRequest('get', [fullPath]));
  502. entry.ctime = ctime;
  503. entry.mtime = mtime;
  504. await this.dbRequest('put', [entry]);
  505. };
  506. const ctime = fromObj.ctime ? fromObj.ctime : Date.now();
  507. switch (fromObj.type) {
  508. // The "from" object is a file
  509. case 'file': {
  510. // Read the file
  511. const file = await this.readFile({
  512. path: from,
  513. directory: fromDirectory,
  514. });
  515. // Optionally remove the file
  516. if (doRename) {
  517. await this.deleteFile({
  518. path: from,
  519. directory: fromDirectory,
  520. });
  521. }
  522. let encoding;
  523. if (!(file.data instanceof Blob) && !this.isBase64String(file.data)) {
  524. encoding = Encoding.UTF8;
  525. }
  526. // Write the file to the new location
  527. const writeResult = await this.writeFile({
  528. path: to,
  529. directory: toDirectory,
  530. data: file.data,
  531. encoding: encoding,
  532. });
  533. // Copy the mtime/ctime of a renamed file
  534. if (doRename) {
  535. await updateTime(to, ctime, fromObj.mtime);
  536. }
  537. // Resolve promise
  538. return writeResult;
  539. }
  540. case 'directory': {
  541. if (toObj) {
  542. throw Error('Cannot move a directory over an existing object');
  543. }
  544. try {
  545. // Create the to directory
  546. await this.mkdir({
  547. path: to,
  548. directory: toDirectory,
  549. recursive: false,
  550. });
  551. // Copy the mtime/ctime of a renamed directory
  552. if (doRename) {
  553. await updateTime(to, ctime, fromObj.mtime);
  554. }
  555. }
  556. catch (e) {
  557. // ignore
  558. }
  559. // Iterate over the contents of the from location
  560. const contents = (await this.readdir({
  561. path: from,
  562. directory: fromDirectory,
  563. })).files;
  564. for (const filename of contents) {
  565. // Move item from the from directory to the to directory
  566. await this._copy({
  567. from: `${from}/${filename.name}`,
  568. to: `${to}/${filename.name}`,
  569. directory: fromDirectory,
  570. toDirectory,
  571. }, doRename);
  572. }
  573. // Optionally remove the original from directory
  574. if (doRename) {
  575. await this.rmdir({
  576. path: from,
  577. directory: fromDirectory,
  578. });
  579. }
  580. }
  581. }
  582. return {
  583. uri: toPath,
  584. };
  585. }
  586. isBase64String(str) {
  587. try {
  588. return btoa(atob(str)) == str;
  589. }
  590. catch (err) {
  591. return false;
  592. }
  593. }
  594. }
  595. FilesystemWeb._debug = true;
  596. //# sourceMappingURL=web.js.map