import { fetcher } from '@cloudron/pankow'; import { sanitize } from '@cloudron/pankow/utils'; const BASE_URL = import.meta.env.BASE_URL || '/'; export function createDirectoryModel(origin, accessToken, api) { const ownersModel = [{ uid: 0, label: 'root' }, { uid: 33, label: 'www-data' }, { uid: 808, label: 'yellowtent' }, { uid: 1000, label: 'cloudron' }, { uid: 1001, label: 'git' }]; return { name: 'DirectoryModel', ownersModel, buildFilePath(filePath, fileName) { // remove leading and trailing slashes while (filePath.startsWith('/')) filePath = filePath.slice(1); while (filePath.endsWith('/')) filePath = filePath.slice(0, -1); return encodeURIComponent(`${filePath}${filePath ? '/' : ''}${fileName}`); }, async listFiles(path) { let error, result; try { result = await fetcher.get(`${origin}/api/v1/${api}/files/${path}`, { access_token: accessToken }); } catch (e) { error = e; } if (error || result.status !== 200) { if (error.status === 404) return []; console.error('Failed to list files', error || result.status); return []; } // this prepares the entries to be compatible with all components result.body.entries.forEach(item => { item.id = item.fileName; item.name = item.fileName; item.folderPath = path; item.modified = new Date(item.mtime); item.type = item.isDirectory ? 'directory' : 'file', item.icon = `${BASE_URL}mime-types/${item.mimeType === 'inode/symlink' ? 'none' : item.mimeType.split('/').join('-')}.svg`; item.previewUrl = ''; // if we have an image, attach previewUrl if (item.mimeType.indexOf('image/') === 0) { if (item.mimeType === 'image/vnd.adobe.photoshop') item.icon = `${BASE_URL}mime-types/image-x-generic.svg`; else item.previewUrl = `${origin}/api/v1/${api}/files/${encodeURIComponent(path + '/' + item.fileName)}?access_token=${accessToken}`; } item.owner = item.uid; if (ownersModel.find((m) => m.uid === item.uid)) item.owner = ownersModel.find((m) => m.uid === item.uid).label; }); return result.body.entries; }, upload(targetDir, file, progressHandler) { // file may contain a file name or a file path + file name const relativefilePath = (file.webkitRelativePath ? file.webkitRelativePath : file.name); const xhr = new XMLHttpRequest(); const req = new Promise(function (resolve, reject) { xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response); } else { reject({ status: xhr.status, statusText: xhr.statusText }); } }); xhr.addEventListener('error', () => { reject({ status: xhr.status, statusText: xhr.statusText }); }); xhr.upload.addEventListener('progress', (event) => { if (event.loaded) progressHandler({ direction: 'upload', loaded: event.loaded}); }); xhr.open('POST', `${origin}/api/v1/${api}/files/${encodeURIComponent(sanitize(targetDir + '/' + relativefilePath))}?access_token=${accessToken}&overwrite=true`); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.send(file); }); // attach for upstream xhr.abort() req.xhr = xhr; return req; }, async newFile(filePath) { await this.save(filePath, ''); }, async newFolder(folderPath) { await fetcher.post(`${origin}/api/v1/${api}/files/${folderPath}`, {}, { directory: true, access_token: accessToken }); }, async remove(filePath) { await fetcher.del(`${origin}/api/v1/${api}/files/${filePath}`, null, { access_token: accessToken }); }, async rename(fromFilePath, toFilePath, overwrite = false) { return await fetcher.put(`${origin}/api/v1/${api}/files/${fromFilePath}`, { action: 'rename', newFilePath: sanitize(toFilePath), overwrite }, { access_token: accessToken }); }, async copy(fromFilePath, toFilePath) { return await fetcher.put(`${origin}/api/v1/${api}/files/${fromFilePath}`, { action: 'copy', newFilePath: sanitize(toFilePath) }, { access_token: accessToken }); }, async chown(filePath, uid) { await fetcher.put(`${origin}/api/v1/${api}/files/${filePath}`, { action: 'chown', uid: uid, recursive: true }, { access_token: accessToken }); }, async extract(path) { await fetcher.put(`${origin}/api/v1/${api}/files/${path}`, { action: 'extract' }, { access_token: accessToken }); }, async download(path) { window.open(`${origin}/api/v1/${api}/files/${path}?download=true&access_token=${accessToken}`); }, async save(filePath, content) { const file = new File([content], 'file'); const req = new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response); } else { reject({ status: xhr.status, statusText: xhr.statusText }); } }); xhr.addEventListener('error', () => { reject({ status: xhr.status, statusText: xhr.statusText }); }); xhr.open('POST', `${origin}/api/v1/${api}/files/${filePath}?access_token=${accessToken}&overwrite=true`); xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('Content-Length', file.size); xhr.send(file); }); await req; }, async getFile(path) { let result; try { result = await fetch(`${origin}/api/v1/${api}/files/${path}?access_token=${accessToken}`); } catch (error) { console.error('Failed to get file', error); return null; } const text = await result.text(); return text; }, async paste(targetDir, action, files) { // this will not overwrite but tries to find a new unique name to past to for (const f in files) { let done = false; let targetPath = targetDir + '/' + files[f].name; while (!done) { let result; if (action === 'cut') result = await this.rename(this.buildFilePath(files[f].folderPath, files[f].name), targetPath); if (action === 'copy') result = await this.copy(this.buildFilePath(files[f].folderPath, files[f].name), targetPath); if (result.status === 404) { throw `Source file "${files[f].name}" not found`; } else if (result.status === 409) { targetPath += '-copy'; continue; } done = true; } } }, getFileUrl(path) { return `${origin}/api/v1/${api}/files/${path}?access_token=${accessToken}`; } }; } export default { createDirectoryModel };