UNPKG

@mieweb/wikigdrive

Version:

Google Drive to MarkDown synchronization

382 lines (381 loc) 20.6 kB
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; import { Controller, ErrorHandler, RouteErrorHandler, RouteResponse, RouteUse } from './Controller.js'; import { MimeTypes } from '../../../model/GoogleFile.js'; import { UserConfigService } from '../../google_folder/UserConfigService.js'; import { DirectoryScanner, isTextFileName } from '../../transform/DirectoryScanner.js'; import { GitScanner } from '../../../git/GitScanner.js'; import { MarkdownTreeProcessor } from '../../transform/MarkdownTreeProcessor.js'; import { clearCachedChanges } from '../../job/JobManagerContainer.js'; import { getContentFileService } from '../../transform/utils.js'; import { LocalLog } from '../../transform/LocalLog.js'; import { GoogleTreeProcessor } from '../../google_folder/GoogleTreeProcessor.js'; export const extToMime = { 'js': 'application/javascript', 'mjs': 'application/javascript', 'css': 'text/css', 'txt': 'text/plain', 'md': 'text/x-markdown', 'htm': 'text/html', 'html': 'text/html', 'svg': 'image/svg+xml' }; // deno-lint-ignore no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars export function convertToPreviewUrl(preview_rewrite_rules, driveId) { return (file) => { for (const preview_rewrite_rule of preview_rewrite_rules.split('\n')) { const preview_rewrite_rule_parts = preview_rewrite_rule.split('!').filter(str => !!str); if (file.path.match(new RegExp(preview_rewrite_rule_parts[0]))) { const previewUrl = file.path.replace(new RegExp(preview_rewrite_rule_parts[0]), preview_rewrite_rule_parts[1]); return { ...file, previewUrl }; } } return { ...file, previewUrl: file.path }; }; } export class ShareErrorHandler extends ErrorHandler { constructor() { super(...arguments); Object.defineProperty(this, "authContainer", { enumerable: true, configurable: true, writable: true, value: void 0 }); } async catch(err) { if (err.message === 'Drive not shared with wikigdrive') { const authConfig = this.authContainer['authConfig']; this.res.status(404).json({ not_registered: true, share_email: authConfig.share_email }); return; } throw err; } } export const CACHE_PATH = '.private/cached_git_status.json'; const workingJobs = {}; export async function getCachedChanges(logger, transformedFileSystem, contentFileService, googleFileSystem) { let mtime = 0; try { const mtimeGit = await transformedFileSystem.getMtime('.git/refs/head/master'); if (mtime < mtimeGit) mtime = mtimeGit; // deno-lint-ignore no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (ignore) { /* empty */ } try { const mtimeContent = await contentFileService.getMtime('.tree.json'); if (mtime < mtimeContent) mtime = mtimeContent; // deno-lint-ignore no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (ignore) { /* empty */ } if (await googleFileSystem.exists(CACHE_PATH)) { const cached = await googleFileSystem.readJson(CACHE_PATH); if (cached?.mtime === mtime) { return cached.changes; } } if (workingJobs[contentFileService.getRealPath()]) { return workingJobs[contentFileService.getRealPath()]; } // eslint-disable-next-line no-async-promise-executor workingJobs[contentFileService.getRealPath()] = new Promise(async (resolve, reject) => { try { const gitScanner = new GitScanner(logger, transformedFileSystem.getRealPath(), 'wikigdrive@wikigdrive.com'); await gitScanner.initialize(); const changes = await gitScanner.changes(); await googleFileSystem.writeJson(CACHE_PATH, { mtime: mtime, changes }); delete workingJobs[contentFileService.getRealPath()]; resolve(changes); } catch (err) { delete workingJobs[contentFileService.getRealPath()]; reject(err); } }); return workingJobs[contentFileService.getRealPath()]; } async function addGitData(treeItems, changes, contentFilePath) { if (contentFilePath.startsWith('/')) { contentFilePath = contentFilePath.substring(1); } for (const treeItem of treeItems) { const change = changes.find(change => change.path === (contentFilePath + treeItem.path).replace(/^\//, '')); if (change) { if (change.state.isNew) { treeItem['status'] = 'N'; } else if (change.state.isModified) { treeItem['status'] = 'M'; } else if (change.state.isDeleted) { treeItem['status'] = 'D'; } } } } export function outputDirectory(treeItem) { const treeItems = [].concat(treeItem.children || []); treeItems.sort((file1, file2) => { if ((MimeTypes.FOLDER_MIME === file1.mimeType) && !(MimeTypes.FOLDER_MIME === file2.mimeType)) { return -1; } if (!(MimeTypes.FOLDER_MIME === file1.mimeType) && (MimeTypes.FOLDER_MIME === file2.mimeType)) { return 1; } if (!file1.fileName || !file2.fileName) { return 0; } return file1.fileName.toLocaleLowerCase().localeCompare(file2.fileName.toLocaleLowerCase()); }); return treeItems; } function inDir(dirPath, filePath) { if (dirPath === filePath) { return true; } return filePath.startsWith(dirPath + '/'); } let FolderController = (() => { var _a; let _classSuper = Controller; let _instanceExtraInitializers = []; let _getFolder_decorators; return _a = class FolderController extends _classSuper { constructor(subPath, filesService, engine) { super(subPath); Object.defineProperty(this, "filesService", { enumerable: true, configurable: true, writable: true, value: (__runInitializers(this, _instanceExtraInitializers), filesService) }); Object.defineProperty(this, "engine", { enumerable: true, configurable: true, writable: true, value: engine }); } async removeFolder(driveId, contentFileService, filePath) { if (filePath.length < 2) { return { removed: false }; } const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); await transformedFileSystem.remove(filePath); // Remove redirs const localLog = new LocalLog(contentFileService); await localLog.load(); const googleFileSystem = await this.filesService.getSubFileService(driveId, ''); const userConfigService = new UserConfigService(googleFileSystem); await userConfigService.load(); const contentDir = (userConfigService.config.transform_subdir || '').startsWith('/') ? (userConfigService.config.transform_subdir || '') : ''; if (await localLog.remove(filePath.substring(contentDir.length))) { await localLog.save(); } await clearCachedChanges(googleFileSystem); const markdownTreeProcessor = new MarkdownTreeProcessor(contentFileService); await markdownTreeProcessor.regenerateTree(driveId); await markdownTreeProcessor.save(); return { removed: true, filePath }; } async getFolder(ctx) { const method = await ctx.routeParamMethod(); const driveId = await ctx.routeParamPath('driveId'); const body = await ctx.routeParamBody(); const filePath = ctx.req.originalUrl.replace('/api/file/' + driveId, '') || '/'; const folderRegistryContainer = this.engine.getContainer('folder_registry'); if (!folderRegistryContainer.hasFolder(driveId)) { ctx.res.status(404).send(JSON.stringify({ message: 'Folder not registered' })); return; } const googleFileSystem = await this.filesService.getSubFileService(driveId, '/'); const userConfigService = new UserConfigService(googleFileSystem); await userConfigService.load(); const transformedFileSystem = await this.filesService.getSubFileService(driveId + '_transform', ''); const contentFileService = await getContentFileService(transformedFileSystem, userConfigService); if (method === 'delete') { const result = await this.removeFolder(driveId, contentFileService, filePath); ctx.res.status(200).send(JSON.stringify(result)); this.engine.emit(driveId, 'toasts:added', { title: 'File deleted: ' + filePath, type: 'tree:changed' }); return; } if (method === 'put') { if (!await transformedFileSystem.exists(filePath)) { ctx.res.status(404).send('Not exist in transformedFileSystem'); return; } await transformedFileSystem.writeFile(filePath, body); await clearCachedChanges(googleFileSystem); this.engine.emit(driveId, 'toasts:added', { title: 'File modified: ' + filePath, type: 'tree:changed' }); } const googleTreeProcessor = new GoogleTreeProcessor(googleFileSystem); await googleTreeProcessor.load(); const markdownTreeProcessor = new MarkdownTreeProcessor(contentFileService); await markdownTreeProcessor.load(); const treeVersion = markdownTreeProcessor.getTreeVersion(); ctx.res.setHeader('wgd-drive-empty', googleTreeProcessor.getTree().length === 0 ? 'true' : 'false'); ctx.res.setHeader('wgd-tree-empty', markdownTreeProcessor.getTree().length === 0 ? 'true' : 'false'); ctx.res.setHeader('wgd-tree-version', treeVersion); ctx.res.setHeader('wgd-content-dir', userConfigService.config.transform_subdir || ''); if (!await transformedFileSystem.exists(filePath)) { ctx.res.status(404).send({ message: 'Not exist in transformedFileSystem' }); return; } if ((userConfigService.config.transform_subdir || '').startsWith('/') && inDir(userConfigService.config.transform_subdir, filePath)) { const prefixed_subdir = userConfigService.config.transform_subdir; const contentFilePath = filePath.replace(prefixed_subdir, '') || '/'; const [treeItem] = contentFilePath === '/' ? await markdownTreeProcessor.getRootItem(driveId) : await markdownTreeProcessor.findByPath(contentFilePath); if (treeItem) { const { previewUrl } = convertToPreviewUrl(userConfigService.config.preview_rewrite_rule || '', driveId)({ path: treeItem.path || '' }); ctx.res.setHeader('wgd-google-parent-id', treeItem.parentId || ''); ctx.res.setHeader('wgd-google-id', treeItem.id || ''); ctx.res.setHeader('wgd-google-version', treeItem.version || ''); ctx.res.setHeader('wgd-google-modified-time', treeItem.modifiedTime || ''); ctx.res.setHeader('wgd-path', treeItem.path || ''); ctx.res.setHeader('wgd-file-name', treeItem.fileName || ''); ctx.res.setHeader('wgd-mime-type', treeItem.mimeType || ''); ctx.res.setHeader('wgd-preview-url', previewUrl); ctx.res.setHeader('wgd-last-author', treeItem.lastAuthor || ''); if (await transformedFileSystem.isDirectory(filePath)) { const changes = await getCachedChanges(ctx.logger, transformedFileSystem, contentFileService, googleFileSystem); const subDir = await transformedFileSystem.getSubFileService(filePath); const map1 = new Map((await this.generateChildren(subDir, driveId, prefixed_subdir, filePath)) .map(element => [element.realFileName, element])); const map2 = new Map(treeItem.children.map(convertToPreviewUrl(userConfigService.config.preview_rewrite_rule || '', driveId)) .map(element => [element.realFileName, element])); treeItem.children = Object.values({ ...Object.fromEntries(map1), ...Object.fromEntries(map2) }); await addGitData(treeItem.children, changes, prefixed_subdir); const treeItems = outputDirectory(treeItem); ctx.res.setHeader('content-type', MimeTypes.FOLDER_MIME); ctx.res.send(JSON.stringify(treeItems)); return; } else { if (treeItem.mimeType) { ctx.res.setHeader('Content-type', treeItem.mimeType); } const buffer = await transformedFileSystem.readBuffer(filePath); ctx.res.send(buffer); return; } } } if (!await transformedFileSystem.exists(filePath)) { ctx.res.status(404).send({ message: 'Not exist in transformedFileSystem' }); return; } if (await transformedFileSystem.isDirectory(filePath)) { const subDir = await transformedFileSystem.getSubFileService(filePath); const treeItem = { fileName: filePath, id: '', parentId: '', path: filePath, realFileName: '', title: '', mimeType: MimeTypes.FOLDER_MIME, children: await this.generateChildren(subDir, driveId, userConfigService.config.transform_subdir || '/', filePath) }; const changes = await getCachedChanges(ctx.logger, transformedFileSystem, contentFileService, googleFileSystem); await addGitData(treeItem.children, changes, ''); treeItem.children = treeItem.children.map(convertToPreviewUrl(userConfigService.config.preview_rewrite_rule, driveId)); const treeItems = outputDirectory(treeItem); ctx.res.setHeader('content-type', MimeTypes.FOLDER_MIME); ctx.res.send(JSON.stringify(treeItems)); return; } else { const ext = await transformedFileSystem.guessExtension(filePath); const mimeType = extToMime[ext] || (isTextFileName(filePath) ? 'text/plain' : undefined); if (mimeType) { ctx.res.setHeader('Content-type', mimeType); } if ('md' === ext) { const { previewUrl } = convertToPreviewUrl(userConfigService.config.preview_rewrite_rule, driveId)(filePath); ctx.res.setHeader('wgd-path', filePath || ''); ctx.res.setHeader('wgd-mime-type', mimeType); ctx.res.setHeader('wgd-preview-url', previewUrl); } const buffer = await transformedFileSystem.readBuffer(filePath); ctx.res.send(buffer); return; } } async generateChildren(transformedFileSystem, driveId, subdir, dirPath) { const scanner = new DirectoryScanner(); const files = await scanner.scan(transformedFileSystem); return Object.values(files) .map(file => { return { fileName: file.fileName, id: subdir === dirPath + file.fileName ? driveId : 'UNKNOWN', parentId: 'UNKNOWN', path: dirPath + file.fileName, realFileName: file.fileName, title: file.title, mimeType: file.mimeType, conflicting: file.type === 'conflict' ? file.conflicting : undefined, redirectTo: file.type === 'redir' ? file.redirectTo : undefined }; }); } }, (() => { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; _getFolder_decorators = [RouteUse('/:driveId'), RouteResponse('stream'), RouteErrorHandler(new ShareErrorHandler())]; __esDecorate(_a, null, _getFolder_decorators, { kind: "method", name: "getFolder", static: false, private: false, access: { has: obj => "getFolder" in obj, get: obj => obj.getFolder }, metadata: _metadata }, null, _instanceExtraInitializers); if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); })(), _a; })(); export default FolderController;