mirror of
https://github.com/kiwix/kiwix-js.git
synced 2025-08-05 12:17:04 -04:00
381 lines
15 KiB
JavaScript
381 lines
15 KiB
JavaScript
/**
|
|
* abstractFilesystemAccess.js: Abstraction layer for file access.
|
|
* This is currently only implemented for FirefoxOS and Standard browser (using File System Access API), but could be extended to
|
|
* Cordova, Electron or other ways to directly browse and read files from the
|
|
* filesystem.
|
|
*
|
|
* Copyright 2014-2023 Kiwix developers and Rishabhg71
|
|
* Licence GPL v3:
|
|
*
|
|
* This file is part of Kiwix.
|
|
*
|
|
* Kiwix is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public Licence as published by
|
|
* the Free Software Foundation, either version 3 of the Licence, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Kiwix is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public Licence for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public Licence
|
|
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import cache from './cache.js';
|
|
import translateUI from './translateUI.js';
|
|
import settingsStore from './settingsStore.js';
|
|
|
|
function StorageFirefoxOS (storage) {
|
|
this._storage = storage;
|
|
this.storageName = storage.storageName;
|
|
}
|
|
|
|
/**
|
|
* Access the given file.
|
|
* @param {String} path absolute path to the file
|
|
* @return {Promise} Promise which is resolved with a HTML5 file object and
|
|
* rejected with an error message.
|
|
*/
|
|
StorageFirefoxOS.prototype.get = function (path) {
|
|
var that = this;
|
|
return new Promise(function (resolve, reject) {
|
|
var request = that._storage.get(path);
|
|
request.onsuccess = function () { resolve(this.result); };
|
|
request.onerror = function () { reject(this.error.name); };
|
|
});
|
|
};
|
|
|
|
// We try to match both a standalone ZIM file (.zim) or
|
|
// the first file of a split ZIM files collection (.zimaa)
|
|
var regexpZIMFileName = /\.zim(aa)?$/i;
|
|
|
|
/**
|
|
* Searches for archive files or directories.
|
|
* @return {Promise} Promise which is resolved with an array of
|
|
* paths and rejected with an error message.
|
|
*/
|
|
StorageFirefoxOS.prototype.scanForArchives = function () {
|
|
var that = this;
|
|
return new Promise(function (resolve, reject) {
|
|
var directories = [];
|
|
var cursor = that._storage.enumerate();
|
|
cursor.onerror = function () {
|
|
reject(cursor.error);
|
|
};
|
|
cursor.onsuccess = function () {
|
|
if (!cursor.result) {
|
|
resolve(directories);
|
|
return;
|
|
}
|
|
var file = cursor.result;
|
|
|
|
if (regexpZIMFileName.test(file.name)) {
|
|
directories.push(file.name);
|
|
}
|
|
|
|
cursor.continue();
|
|
};
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Browse a path through DeviceStorage API
|
|
* @param path Path where to look for files
|
|
* @return {DOMCursor} Cursor of files found in given path
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
StorageFirefoxOS.prototype.enumerate = function (path) {
|
|
return this._storage.enumerate();
|
|
};
|
|
|
|
// refer to this article for easy explanation of File System API https://developer.chrome.com/articles/file-system-access/
|
|
|
|
/**
|
|
* @param {Array<string>} files All the File names to be shown in the dropdown
|
|
* @param {string} selectedFile The name of the file to be selected in the dropdown
|
|
* @returns {Array<string>} Array of unique filenames (if a split zim is considered a single file)
|
|
*/
|
|
function updateZimDropdownOptions (files, selectedFile) {
|
|
const isFireFoxOsNativeFileApiAvailable = typeof navigator.getDeviceStorages === 'function';
|
|
// This will make sure that there is no race around condition when platform is firefox os
|
|
// as other function will handle the dropdown UI updates
|
|
if (isFireFoxOsNativeFileApiAvailable) return // do nothing let other function handle it
|
|
|
|
const select = document.getElementById('archiveList');
|
|
const options = [];
|
|
let count = 0;
|
|
select.innerHTML = '';
|
|
if (files.length !== 0) {
|
|
const placeholderOption = new Option(translateUI.t('configure-select-file-first-option'), '');
|
|
placeholderOption.disabled = true;
|
|
select.appendChild(placeholderOption);
|
|
};
|
|
// Create a new option for each fileName or for the zimaa part of a split archive
|
|
files.forEach((fileName) => {
|
|
if (/\.zim(aa)?$/i.test(fileName)) {
|
|
options.push(new Option(fileName, fileName));
|
|
select.appendChild(new Option(fileName, fileName));
|
|
count++;
|
|
}
|
|
});
|
|
select.value = selectedFile;
|
|
document.getElementById('numberOfFilesCount').style.display = '';
|
|
document.getElementById('fileCountDisplay').style.display = '';
|
|
document.getElementById('numberOfFilesCount').innerText = count.toString();
|
|
document.getElementById('fileCountDisplay').innerText = translateUI.t('configure-select-file-numbers');
|
|
}
|
|
|
|
/**
|
|
* Opens the File System API to select a directory
|
|
* @returns {Promise<Array<File>>} Previously selected file if available in selected folder
|
|
*/
|
|
async function selectDirectoryFromPickerViaFileSystemApi () {
|
|
const handle = await window.showDirectoryPicker();
|
|
const fileNames = [];
|
|
const previousZimFile = []
|
|
const lastZimName = settingsStore.getItem('previousZimFileName') || '';
|
|
const lastZimNameWithoutExtension = lastZimName.replace(/\.zim\w?\w?$/i, '');
|
|
// Iterate over all files in directory, store an array of ZIM files, and get the previously selectee ZIM file if it exists
|
|
for await (const entry of handle.values()) {
|
|
if (entry.kind === 'file' && /\.zim\w?\w?$/i.test(entry.name)) {
|
|
fileNames.push(entry.name);
|
|
if (!entry.name.indexOf(lastZimNameWithoutExtension)) {
|
|
previousZimFile.push(await entry.getFile());
|
|
}
|
|
}
|
|
}
|
|
settingsStore.setItem('zimFilenames', fileNames.join('|'), Infinity);
|
|
updateZimDropdownOptions(fileNames, previousZimFile.length !== 0 ? lastZimName : '');
|
|
cache.idxDB('zimFiles', handle, function () {
|
|
// save file in DB
|
|
});
|
|
return previousZimFile;
|
|
}
|
|
|
|
/**
|
|
* Opens the File System API to select a file
|
|
* @returns {Promise<Array<File>>} The selected file from picker
|
|
*/
|
|
async function selectFileFromPickerViaFileSystemApi () {
|
|
const fileHandles = await window.showOpenFilePicker({ multiple: false });
|
|
const [selectedFile] = fileHandles;
|
|
const file = await selectedFile.getFile();
|
|
const filenameList = [selectedFile.name];
|
|
settingsStore.setItem('zimFilenames', filenameList.join('|'));
|
|
cache.idxDB('zimFiles', selectedFile, function () {
|
|
// file saved in DB
|
|
updateZimDropdownOptions(filenameList, selectedFile.name);
|
|
});
|
|
return [file];
|
|
}
|
|
|
|
/**
|
|
* Gets the selected zim file from the IndexedDB
|
|
* @param {string} selectedFilename The name of the file to get back from DB
|
|
* @returns {Promise<Array<File>>} The selected File Object from cache
|
|
*/
|
|
function getSelectedZimFromCache (selectedFilename) {
|
|
return new Promise((resolve, reject) => {
|
|
cache.idxDB('zimFiles', async function (fileOrDirHandle) {
|
|
if (!fileOrDirHandle) {
|
|
return reject(new Error('No file or directory selected'));
|
|
}
|
|
// Request permission if not already granted
|
|
if ((await fileOrDirHandle.queryPermission()) !== 'granted') {
|
|
try {
|
|
await fileOrDirHandle.requestPermission();
|
|
} catch (error) {
|
|
return reject(new Error('Permission denied', error));
|
|
}
|
|
}
|
|
if (fileOrDirHandle.kind === 'directory') {
|
|
const files = [];
|
|
for await (const entry of fileOrDirHandle.values()) {
|
|
const filenameWithoutExtension = selectedFilename.replace(/\.zim\w?\w?$/i, '');
|
|
// const regex = new RegExp(`\\${filenameWithoutExtension}.zim\\w\\w$`, 'i');
|
|
if (!entry.name.indexOf(filenameWithoutExtension)) {
|
|
files.push(await entry.getFile());
|
|
}
|
|
}
|
|
resolve(files);
|
|
} else {
|
|
const file = await fileOrDirHandle.getFile();
|
|
resolve([file]);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object.<number, File>} WebkitFileList
|
|
*/
|
|
|
|
/**
|
|
* Gets the selected zim file from the WebkitFileList
|
|
*
|
|
* @param {WebkitFileList} fileList The WebkitFileList to get the selected file from
|
|
* @param {string} filename The name of the file to get back from webkitFileList
|
|
* @returns {Array<File>} The selected Files Object from webkitFileList
|
|
*/
|
|
function getSelectedZimFromWebkitList (fileList, filename) {
|
|
const filenameWithoutExtension = filename.replace(/\.zim\w?\w?$/i, '');
|
|
const files = [];
|
|
for (const file of fileList) {
|
|
// If the file.name begins with the filenameWithoutExtension, then it matches (may match mutliple split ZIM files)
|
|
if (!file.name.indexOf(filenameWithoutExtension)) {
|
|
files.push(file);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Loads the Previously loaded zim filename(s) via local storage
|
|
*/
|
|
function loadPreviousZimFile () {
|
|
// If we call `updateZimDropdownOptions` without any delay it will run before the internationalization is initialized
|
|
// It's a bit hacky but it works and I am not sure if there is any other way ATM
|
|
setTimeout(() => {
|
|
if (window.params.isFileSystemApiSupported || window.params.isWebkitDirApiSupported) {
|
|
const filenames = settingsStore.getItem('zimFilenames');
|
|
if (filenames) updateZimDropdownOptions(filenames.split('|'), '');
|
|
}
|
|
}, 200);
|
|
}
|
|
|
|
/**
|
|
* Handles selecting a directory from the webkitdirectory picker, setting the UI and storing the selected files
|
|
*
|
|
* @param {WebkitFileList} fileList The list returned from the webkitdirectory picker
|
|
* @returns An object containing all the ZIM files in the directory and the previously selected ZIM fileset if available
|
|
*/
|
|
function selectDirectoryFromPickerViaWebkit (fileList) {
|
|
const zimFiles = [];
|
|
const filenames = [];
|
|
const previousZimFile = []
|
|
const lastFilename = settingsStore.getItem('previousZimFileName') || '';
|
|
const filenameWithoutExtension = lastFilename.replace(/\.zim\w?\w?$/i, '');
|
|
for (const file of fileList) {
|
|
// Only add ZIM files in top directory to the filenames array. To do this, we must exclude ZIM files that contain more than one '/' in their path.
|
|
// This is because most browsers using this API will return the full path of the file including the directory it is in (but Chromium < 72 will not return the full path, so
|
|
// in that browser, we will get some files that are in subdirectories). See MDN webkitdirectory documentation for more information.
|
|
if (/^[^/]+\/[^/]+\.zim\w?\w?$/i.test(file.webkitRelativePath)) {
|
|
zimFiles.push(file);
|
|
filenames.push(file.name);
|
|
// If the file.name begins with the filenameWithoutExtension...
|
|
if (filenameWithoutExtension && !file.name.indexOf(filenameWithoutExtension)) {
|
|
previousZimFile.push(file);
|
|
}
|
|
}
|
|
}
|
|
settingsStore.setItem('zimFilenames', filenames.join('|'), Infinity);
|
|
updateZimDropdownOptions(filenames, previousZimFile.length ? lastFilename : '');
|
|
return { files: zimFiles, selectedFile: previousZimFile };
|
|
}
|
|
|
|
/**
|
|
* Handles the folder drop event via File System API
|
|
* @param {DragEvent} packet The DragEvent packet
|
|
* @returns {Promise<boolean>} Whether the dropped item is a file or directory
|
|
*/
|
|
async function handleFolderOrFileDropViaFileSystemAPI (packet) {
|
|
if (!window.params.isFileSystemApiSupported) return true;
|
|
|
|
// Only runs when browser support File System API
|
|
const fileInfo = packet.dataTransfer.items[0];
|
|
const fileOrDirHandle = await fileInfo.getAsFileSystemHandle();
|
|
if (fileOrDirHandle.kind === 'file') {
|
|
settingsStore.setItem([fileOrDirHandle.name], [fileOrDirHandle.name].join('|'), Infinity);
|
|
cache.idxDB('zimFiles', fileOrDirHandle, function () {
|
|
// save file in DB
|
|
updateZimDropdownOptions([fileOrDirHandle.name], fileOrDirHandle.name);
|
|
});
|
|
settingsStore.setItem('zimFilenames', [fileOrDirHandle.name].join('|'), Infinity);
|
|
return true;
|
|
}
|
|
if (fileOrDirHandle.kind === 'directory') {
|
|
const fileNames = [];
|
|
for await (const entry of fileOrDirHandle.values()) {
|
|
fileNames.push(entry.name);
|
|
}
|
|
settingsStore.setItem('zimFilenames', fileNames.join('|'), Infinity);
|
|
cache.idxDB('zimFiles', fileOrDirHandle, function () {
|
|
updateZimDropdownOptions(fileNames, '');
|
|
// save file in DB
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the folder drop event via WebkitGetAsEntry
|
|
* @param {DragEvent} event The DragEvent packet
|
|
* @returns {Promise<{loadZim: boolean, files: Array<File>} | void>} Whether the dropped item is a file or directory and FileList
|
|
*/
|
|
async function handleFolderOrFileDropViaWebkit (event) {
|
|
var dt = event.dataTransfer;
|
|
var entry = dt.items[0].webkitGetAsEntry();
|
|
if (entry.isFile) {
|
|
settingsStore.setItem('zimFilenames', [entry.name].join('|'), Infinity);
|
|
updateZimDropdownOptions([entry.name], entry.name);
|
|
return { loadZim: true, files: [entry.file] };
|
|
} else if (entry.isDirectory) {
|
|
var reader = entry.createReader();
|
|
const files = await getFilesFromReader(reader);
|
|
const fileNames = [];
|
|
files.forEach(function (file) {
|
|
fileNames.push(file.name)
|
|
});
|
|
settingsStore.setItem('zimFilenames', fileNames.join('|'), Infinity);
|
|
updateZimDropdownOptions(fileNames, '');
|
|
return { loadZim: false, files: files };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the files from the FileSystemReader
|
|
* @param {FileSystemDirectoryReader} reader The FileSystemReader to get files from
|
|
* @returns {Promise<Array<File>>} The files from the reader
|
|
*/
|
|
async function getFilesFromReader (reader) {
|
|
const files = [];
|
|
// eslint-disable-next-line no-unused-vars
|
|
const promise = new Promise(function (resolve, _reject) {
|
|
reader.readEntries(function (entries) {
|
|
resolve(entries);
|
|
});
|
|
});
|
|
const entries = await promise;
|
|
|
|
for (let index = 0; index < entries.length; index++) {
|
|
const fileOrDir = entries[index];
|
|
if (fileOrDir.isFile) {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const filePromise = await new Promise(function (resolve, _reject) {
|
|
fileOrDir.file(function (file) {
|
|
resolve(file);
|
|
});
|
|
});
|
|
files.push(filePromise);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export default {
|
|
StorageFirefoxOS: StorageFirefoxOS,
|
|
updateZimDropdownOptions: updateZimDropdownOptions,
|
|
selectDirectoryFromPickerViaFileSystemApi: selectDirectoryFromPickerViaFileSystemApi,
|
|
selectFileFromPickerViaFileSystemApi: selectFileFromPickerViaFileSystemApi,
|
|
getSelectedZimFromCache: getSelectedZimFromCache,
|
|
loadPreviousZimFile: loadPreviousZimFile,
|
|
handleFolderOrFileDropViaWebkit: handleFolderOrFileDropViaWebkit,
|
|
handleFolderOrFileDropViaFileSystemAPI: handleFolderOrFileDropViaFileSystemAPI,
|
|
getSelectedZimFromWebkitList: getSelectedZimFromWebkitList,
|
|
selectDirectoryFromPickerViaWebkit: selectDirectoryFromPickerViaWebkit
|
|
};
|