mirror of
https://github.com/ApfelTeeSaft/R2-Explorer.git
synced 2026-08-26 19:33:39 +00:00
Improve code quality (#67)
* Migrate to biomejs * Cleanup and add workers-ci method * Started refactoring worker into hono + chanfana * Finish api worker refactor * Lint project * Lint project
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
packages/github-action/*
|
||||
packages/worker/bin/*
|
||||
packages/worker/config/*
|
||||
packages/dashboard/quasar.config.js
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"typescript",
|
||||
"prettier"
|
||||
],
|
||||
"ignorePatterns": [],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn"
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": []
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noExplicitAny": "off",
|
||||
"noImplicitAnyLet": "off"
|
||||
},
|
||||
"style": {
|
||||
"noParameterAssign": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off",
|
||||
"useLiteralKeys": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noAccumulatingSpread": "off",
|
||||
"noDelete": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-23
@@ -1,25 +1,21 @@
|
||||
{
|
||||
"name": "r2-explorer-root",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"lint": "(eslint . || (eslint . --fix; exit 1)) && (prettier --check '*.{json,js}' 'packages/*/src/**/*.{js,ts}' || (prettier -w '*.{json,js}' 'packages/*/src/**/*.{js,ts}'; exit 1))",
|
||||
"build-dashboard": "pnpm run --filter r2-explorer-dashboard build",
|
||||
"build-worker": "pnpm run --filter r2-explorer build",
|
||||
"build": "pnpm build-dashboard && pnpm build-worker",
|
||||
"deploy-dashboard": "pnpm run --filter r2-explorer-dashboard deploy",
|
||||
"deploy-dashboard-dev": "pnpm run --filter r2-explorer-dashboard deploy-dev",
|
||||
"deploy-dev-worker": "pnpm run --filter r2-explorer-dev-worker deploy",
|
||||
"publish-npm": "pnpm run --filter worker publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "0.17.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-typescript": "^3.0.0",
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
"name": "r2-explorer-root",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"workers-ci": "export WORKERS_CI=1 && node packages/github-action/prepareDeploy.js",
|
||||
"lint": "npx @biomejs/biome check template/ packages/dashboard/src packages/worker/src || (npx @biomejs/biome check --write template/ packages/dashboard/src packages/worker/src/; exit 1)",
|
||||
"build-dashboard": "pnpm run --filter r2-explorer-dashboard build",
|
||||
"build-worker": "pnpm run --filter r2-explorer build",
|
||||
"build": "pnpm build-dashboard && pnpm build-worker",
|
||||
"deploy-dashboard": "pnpm run --filter r2-explorer-dashboard deploy",
|
||||
"deploy-dashboard-dev": "pnpm run --filter r2-explorer-dashboard deploy-dev",
|
||||
"deploy-dev-worker": "pnpm run --filter r2-explorer-dev-worker deploy",
|
||||
"publish-npm": "pnpm run --filter worker publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"husky": "^9.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"src/*": [
|
||||
"src/*"
|
||||
],
|
||||
"app/*": [
|
||||
"*"
|
||||
],
|
||||
"components/*": [
|
||||
"src/components/*"
|
||||
],
|
||||
"layouts/*": [
|
||||
"src/layouts/*"
|
||||
],
|
||||
"pages/*": [
|
||||
"src/pages/*"
|
||||
],
|
||||
"assets/*": [
|
||||
"src/assets/*"
|
||||
],
|
||||
"boot/*": [
|
||||
"src/boot/*"
|
||||
],
|
||||
"stores/*": [
|
||||
"src/stores/*"
|
||||
],
|
||||
"vue$": [
|
||||
"node_modules/vue/dist/vue.runtime.esm-bundler.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
".quasar",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"src/*": ["src/*"],
|
||||
"app/*": ["*"],
|
||||
"components/*": ["src/components/*"],
|
||||
"layouts/*": ["src/layouts/*"],
|
||||
"pages/*": ["src/pages/*"],
|
||||
"assets/*": ["src/assets/*"],
|
||||
"boot/*": ["src/boot/*"],
|
||||
"stores/*": ["src/stores/*"],
|
||||
"vue$": ["node_modules/vue/dist/vue.runtime.esm-bundler.js"]
|
||||
}
|
||||
},
|
||||
"exclude": ["dist", ".quasar", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
{
|
||||
"name": "r2-explorer-dashboard",
|
||||
"version": "0.0.1",
|
||||
"description": "R2-Explorer",
|
||||
"productName": "r2-explorer",
|
||||
"author": "Gabriel Massadas",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"dev": "quasar dev",
|
||||
"build": "quasar build",
|
||||
"deploy": "wrangler pages deploy --branch main --project-name r2-explorer-dashboard dist/spa/",
|
||||
"deploy-dev": "wrangler pages deploy --branch dev --project-name r2-explorer-dashboard dist/spa/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.9",
|
||||
"axios": "^1.2.1",
|
||||
"fflate": "^0.8.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
"pdfvuer": "^2.0.1",
|
||||
"pinia": "^2.0.11",
|
||||
"postal-mime": "^2.0.1",
|
||||
"quasar": "^2.14.7",
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.8.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"postcss": "^8.4.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20 || ^18 || ^16 || ^14.19",
|
||||
"npm": ">= 6.13.4",
|
||||
"yarn": ">= 1.21.1"
|
||||
}
|
||||
"name": "r2-explorer-dashboard",
|
||||
"version": "0.0.1",
|
||||
"description": "R2-Explorer",
|
||||
"productName": "r2-explorer",
|
||||
"author": "Gabriel Massadas",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"dev": "quasar dev",
|
||||
"build": "quasar build",
|
||||
"deploy": "wrangler pages deploy --branch main --project-name r2-explorer-dashboard dist/spa/",
|
||||
"deploy-dev": "wrangler pages deploy --branch dev --project-name r2-explorer-dashboard dist/spa/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.16.9",
|
||||
"axios": "^1.2.1",
|
||||
"fflate": "^0.8.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
"pdfvuer": "^2.0.1",
|
||||
"pinia": "^2.0.11",
|
||||
"postal-mime": "^2.0.1",
|
||||
"quasar": "^2.14.7",
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.8.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"postcss": "^8.4.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20 || ^18 || ^16 || ^14.19",
|
||||
"npm": ">= 6.13.4",
|
||||
"yarn": ">= 1.21.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// https://github.com/postcss/autoprefixer
|
||||
require('autoprefixer')({
|
||||
overrideBrowserslist: [
|
||||
'last 4 Chrome versions',
|
||||
'last 4 Firefox versions',
|
||||
'last 4 Edge versions',
|
||||
'last 4 Safari versions',
|
||||
'last 4 Android versions',
|
||||
'last 4 ChromeAndroid versions',
|
||||
'last 4 FirefoxAndroid versions',
|
||||
'last 4 iOS versions'
|
||||
]
|
||||
})
|
||||
plugins: [
|
||||
// https://github.com/postcss/autoprefixer
|
||||
require("autoprefixer")({
|
||||
overrideBrowserslist: [
|
||||
"last 4 Chrome versions",
|
||||
"last 4 Firefox versions",
|
||||
"last 4 Edge versions",
|
||||
"last 4 Safari versions",
|
||||
"last 4 Android versions",
|
||||
"last 4 ChromeAndroid versions",
|
||||
"last 4 FirefoxAndroid versions",
|
||||
"last 4 iOS versions",
|
||||
],
|
||||
}),
|
||||
|
||||
// https://github.com/elchininet/postcss-rtlcss
|
||||
// If you want to support RTL css, then
|
||||
// 1. yarn/npm install postcss-rtlcss
|
||||
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||
// 3. uncomment the following line:
|
||||
// require('postcss-rtlcss')
|
||||
]
|
||||
}
|
||||
// https://github.com/elchininet/postcss-rtlcss
|
||||
// If you want to support RTL css, then
|
||||
// 1. yarn/npm install postcss-rtlcss
|
||||
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||
// 3. uncomment the following line:
|
||||
// require('postcss-rtlcss')
|
||||
],
|
||||
};
|
||||
|
||||
+154
-171
@@ -8,210 +8,193 @@
|
||||
// Configuration for your app
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||
|
||||
const { configure } = require("quasar/wrappers");
|
||||
|
||||
const { configure } = require('quasar/wrappers');
|
||||
module.exports = configure((/* ctx */) => ({
|
||||
// eslint: {
|
||||
// fix: true,
|
||||
// include: [],
|
||||
// exclude: [],
|
||||
// rawOptions: {},
|
||||
// warnings: true,
|
||||
// errors: true
|
||||
// },
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
|
||||
// preFetch: true,
|
||||
|
||||
module.exports = configure(function (/* ctx */) {
|
||||
return {
|
||||
// eslint: {
|
||||
// fix: true,
|
||||
// include: [],
|
||||
// exclude: [],
|
||||
// rawOptions: {},
|
||||
// warnings: true,
|
||||
// errors: true
|
||||
// },
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: ["axios", "auth", "bus"],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
|
||||
// preFetch: true,
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: ["app.scss"],
|
||||
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||
boot: [
|
||||
'axios',
|
||||
'auth',
|
||||
'bus',
|
||||
],
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
// 'mdi-v5',
|
||||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'line-awesome',
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: [
|
||||
'app.scss'
|
||||
],
|
||||
"roboto-font", // optional, you are not bound to it
|
||||
"material-icons", // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
// 'mdi-v5',
|
||||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'line-awesome',
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||
build: {
|
||||
env: require("dotenv").config().parsed,
|
||||
target: {
|
||||
browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"],
|
||||
node: "node16",
|
||||
},
|
||||
|
||||
'roboto-font', // optional, you are not bound to it
|
||||
'material-icons', // optional, you are not bound to it
|
||||
],
|
||||
vueRouterMode: "history", // available values: 'hash', 'history'
|
||||
// vueRouterBase,
|
||||
// vueDevtools,
|
||||
// vueOptionsAPI: false,
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||
build: {
|
||||
env: require('dotenv').config().parsed,
|
||||
target: {
|
||||
browser: [ 'es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1' ],
|
||||
node: 'node16'
|
||||
},
|
||||
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||
|
||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||
// vueRouterBase,
|
||||
// vueDevtools,
|
||||
// vueOptionsAPI: false,
|
||||
publicPath: "/",
|
||||
// analyze: true,
|
||||
// env: {},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: false,
|
||||
// polyfillModulePreload: true,
|
||||
// distDir
|
||||
|
||||
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||
// extendViteConf (viteConf) {},
|
||||
// viteVuePluginOptions: {},
|
||||
|
||||
publicPath: '/',
|
||||
// analyze: true,
|
||||
// env: {},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: false,
|
||||
// polyfillModulePreload: true,
|
||||
// distDir
|
||||
// vitePlugins: [
|
||||
// [ 'package-name', { ..options.. } ]
|
||||
// ]
|
||||
},
|
||||
|
||||
// extendViteConf (viteConf) {},
|
||||
// viteVuePluginOptions: {},
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
devServer: {
|
||||
// https: true
|
||||
open: true, // opens browser window automatically
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||
framework: {
|
||||
config: {},
|
||||
|
||||
// vitePlugins: [
|
||||
// [ 'package-name', { ..options.. } ]
|
||||
// ]
|
||||
},
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
// lang: 'en-US', // Quasar language pack
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
devServer: {
|
||||
// https: true
|
||||
open: true // opens browser window automatically
|
||||
},
|
||||
// For special cases outside of where the auto-import strategy can have an impact
|
||||
// (like functional components as one of the examples),
|
||||
// you can manually specify Quasar components/directives to be available everywhere:
|
||||
//
|
||||
// components: [],
|
||||
// directives: [],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||
framework: {
|
||||
config: {},
|
||||
// Quasar plugins
|
||||
plugins: ["Notify"],
|
||||
},
|
||||
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
// lang: 'en-US', // Quasar language pack
|
||||
// animations: 'all', // --- includes all animations
|
||||
// https://v2.quasar.dev/options/animations
|
||||
animations: [],
|
||||
|
||||
// For special cases outside of where the auto-import strategy can have an impact
|
||||
// (like functional components as one of the examples),
|
||||
// you can manually specify Quasar components/directives to be available everywhere:
|
||||
//
|
||||
// components: [],
|
||||
// directives: [],
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
|
||||
// sourceFiles: {
|
||||
// rootComponent: 'src/App.vue',
|
||||
// router: 'src/router/index',
|
||||
// store: 'src/store/index',
|
||||
// registerServiceWorker: 'src-pwa/register-service-worker',
|
||||
// serviceWorker: 'src-pwa/custom-service-worker',
|
||||
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||
// electronMain: 'src-electron/electron-main',
|
||||
// electronPreload: 'src-electron/electron-preload'
|
||||
// },
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [
|
||||
'Notify'
|
||||
]
|
||||
},
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
||||
ssr: {
|
||||
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||
// will mess up SSR
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
// https://v2.quasar.dev/options/animations
|
||||
animations: [],
|
||||
// extendSSRWebserverConf (esbuildConf) {},
|
||||
// extendPackageJson (json) {},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
|
||||
// sourceFiles: {
|
||||
// rootComponent: 'src/App.vue',
|
||||
// router: 'src/router/index',
|
||||
// store: 'src/store/index',
|
||||
// registerServiceWorker: 'src-pwa/register-service-worker',
|
||||
// serviceWorker: 'src-pwa/custom-service-worker',
|
||||
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||
// electronMain: 'src-electron/electron-main',
|
||||
// electronPreload: 'src-electron/electron-preload'
|
||||
// },
|
||||
pwa: false,
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
||||
ssr: {
|
||||
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||
// will mess up SSR
|
||||
// manualStoreHydration: true,
|
||||
// manualPostHydrationTrigger: true,
|
||||
|
||||
// extendSSRWebserverConf (esbuildConf) {},
|
||||
// extendPackageJson (json) {},
|
||||
prodPort: 3000, // The default port that the production server should use
|
||||
// (gets superseded if process.env.PORT is specified at runtime)
|
||||
|
||||
pwa: false,
|
||||
middlewares: [
|
||||
"render", // keep this as last one
|
||||
],
|
||||
},
|
||||
|
||||
// manualStoreHydration: true,
|
||||
// manualPostHydrationTrigger: true,
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||
pwa: {
|
||||
workboxMode: "generateSW", // or 'injectManifest'
|
||||
injectPwaMetaTags: true,
|
||||
swFilename: "sw.js",
|
||||
manifestFilename: "manifest.json",
|
||||
useCredentialsForManifestTag: false,
|
||||
// useFilenameHashes: true,
|
||||
// extendGenerateSWOptions (cfg) {}
|
||||
// extendInjectManifestOptions (cfg) {},
|
||||
// extendManifestJson (json) {}
|
||||
// extendPWACustomSWConf (esbuildConf) {}
|
||||
},
|
||||
|
||||
prodPort: 3000, // The default port that the production server should use
|
||||
// (gets superseded if process.env.PORT is specified at runtime)
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
|
||||
cordova: {
|
||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||
},
|
||||
|
||||
middlewares: [
|
||||
'render' // keep this as last one
|
||||
]
|
||||
},
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
|
||||
capacitor: {
|
||||
hideSplashscreen: true,
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||
pwa: {
|
||||
workboxMode: 'generateSW', // or 'injectManifest'
|
||||
injectPwaMetaTags: true,
|
||||
swFilename: 'sw.js',
|
||||
manifestFilename: 'manifest.json',
|
||||
useCredentialsForManifestTag: false,
|
||||
// useFilenameHashes: true,
|
||||
// extendGenerateSWOptions (cfg) {}
|
||||
// extendInjectManifestOptions (cfg) {},
|
||||
// extendManifestJson (json) {}
|
||||
// extendPWACustomSWConf (esbuildConf) {}
|
||||
},
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
||||
electron: {
|
||||
// extendElectronMainConf (esbuildConf)
|
||||
// extendElectronPreloadConf (esbuildConf)
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
|
||||
cordova: {
|
||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||
},
|
||||
// specify the debugging port to use for the Electron app when running in development mode
|
||||
inspectPort: 5858,
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
|
||||
capacitor: {
|
||||
hideSplashscreen: true
|
||||
},
|
||||
bundler: "packager", // 'packager' or 'builder'
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
||||
electron: {
|
||||
// extendElectronMainConf (esbuildConf)
|
||||
// extendElectronPreloadConf (esbuildConf)
|
||||
packager: {
|
||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||
// OS X / Mac App Store
|
||||
// appBundleId: '',
|
||||
// appCategoryType: '',
|
||||
// osxSign: '',
|
||||
// protocol: 'myapp://path',
|
||||
// Windows only
|
||||
// win32metadata: { ... }
|
||||
},
|
||||
|
||||
// specify the debugging port to use for the Electron app when running in development mode
|
||||
inspectPort: 5858,
|
||||
builder: {
|
||||
// https://www.electron.build/configuration/configuration
|
||||
|
||||
bundler: 'packager', // 'packager' or 'builder'
|
||||
appId: "r2-explorer-dashboard",
|
||||
},
|
||||
},
|
||||
|
||||
packager: {
|
||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||
bex: {
|
||||
contentScripts: ["my-content-script"],
|
||||
|
||||
// OS X / Mac App Store
|
||||
// appBundleId: '',
|
||||
// appCategoryType: '',
|
||||
// osxSign: '',
|
||||
// protocol: 'myapp://path',
|
||||
|
||||
// Windows only
|
||||
// win32metadata: { ... }
|
||||
},
|
||||
|
||||
builder: {
|
||||
// https://www.electron.build/configuration/configuration
|
||||
|
||||
appId: 'r2-explorer-dashboard'
|
||||
}
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||
bex: {
|
||||
contentScripts: [
|
||||
'my-content-script'
|
||||
],
|
||||
|
||||
// extendBexScriptsConf (esbuildConf) {}
|
||||
// extendBexManifestJson (json) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
// extendBexScriptsConf (esbuildConf) {}
|
||||
// extendBexManifestJson (json) {}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App'
|
||||
})
|
||||
name: "App",
|
||||
});
|
||||
</script>
|
||||
|
||||
+240
-219
@@ -1,257 +1,278 @@
|
||||
import { api } from "boot/axios";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
|
||||
export const ROOT_FOLDER = "IA==" // IA== is a space
|
||||
export const ROOT_FOLDER = "IA=="; // IA== is a space
|
||||
|
||||
function mapFile(obj, prefix) {
|
||||
const date = new Date(obj.uploaded)
|
||||
const date = new Date(obj.uploaded);
|
||||
|
||||
return {
|
||||
...obj,
|
||||
hash: encode(obj.key),
|
||||
nameHash: encode(obj.key.replace(prefix, '')),
|
||||
name: obj.key.replace(prefix, ''),
|
||||
lastModified: timeSince(date),
|
||||
timestamp: date.getTime(),
|
||||
size: bytesToSize(obj.size),
|
||||
sizeRaw: obj.size,
|
||||
type: 'file',
|
||||
icon: 'article',
|
||||
color: 'grey',
|
||||
}
|
||||
return {
|
||||
...obj,
|
||||
hash: encode(obj.key),
|
||||
nameHash: encode(obj.key.replace(prefix, "")),
|
||||
name: obj.key.replace(prefix, ""),
|
||||
lastModified: timeSince(date),
|
||||
timestamp: date.getTime(),
|
||||
size: bytesToSize(obj.size),
|
||||
sizeRaw: obj.size,
|
||||
type: "file",
|
||||
icon: "article",
|
||||
color: "grey",
|
||||
};
|
||||
}
|
||||
|
||||
export const timeSince = (date) => {
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
|
||||
let interval = seconds / 31536000;
|
||||
let calc;
|
||||
let interval = seconds / 31536000;
|
||||
let calc;
|
||||
|
||||
if (interval > 1) {
|
||||
// calc = Math.floor(interval)
|
||||
// return calc + (calc === 1 ? ' year' : ' years')
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
// calc = Math.floor(interval)
|
||||
// return calc + (calc === 1 ? ' month' : ' months')
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
// calc = Math.floor(interval)
|
||||
// return calc + (calc === 1 ? ' day' : ' days')
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
calc = Math.floor(interval);
|
||||
return calc + (calc === 1 ? " hour" : " hours");
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
calc = Math.floor(interval);
|
||||
return calc + (calc === 1 ? " minute" : " minutes");
|
||||
}
|
||||
if (interval > 1) {
|
||||
// calc = Math.floor(interval)
|
||||
// return calc + (calc === 1 ? ' year' : ' years')
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
// calc = Math.floor(interval)
|
||||
// return calc + (calc === 1 ? ' month' : ' months')
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
// calc = Math.floor(interval)
|
||||
// return calc + (calc === 1 ? ' day' : ' days')
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
calc = Math.floor(interval);
|
||||
return calc + (calc === 1 ? " hour" : " hours");
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
calc = Math.floor(interval);
|
||||
return calc + (calc === 1 ? " minute" : " minutes");
|
||||
}
|
||||
|
||||
calc = Math.floor(interval);
|
||||
return calc + (calc === 1 ? " second" : " seconds");
|
||||
calc = Math.floor(interval);
|
||||
return calc + (calc === 1 ? " second" : " seconds");
|
||||
};
|
||||
export const bytesToSize = (bytes) => {
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
if (bytes === 0) return "0 Byte";
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return Math.round(bytes / Math.pow(1024, i), 2) + " " + sizes[i];
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
if (bytes === 0) return "0 Byte";
|
||||
const i = Number.parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
return `${Math.round(bytes / 1024 ** i, 2)} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export const bytesToMegabytes = (bytes) => {
|
||||
return Math.round(bytes / Math.pow(1024, 2));
|
||||
return Math.round(bytes / 1024 ** 2);
|
||||
};
|
||||
|
||||
export const encode = (key) => {
|
||||
if (key && key !== '/' && key.startsWith('/')) {
|
||||
// File keys should never start with /
|
||||
key = key.slice(1)
|
||||
}
|
||||
return btoa(unescape(encodeURIComponent(key)));
|
||||
if (key && key !== "/" && key.startsWith("/")) {
|
||||
// File keys should never start with /
|
||||
key = key.slice(1);
|
||||
}
|
||||
return btoa(unescape(encodeURIComponent(key)));
|
||||
};
|
||||
|
||||
export const decode = (key) => {
|
||||
return decodeURIComponent(escape(atob(key)));
|
||||
return decodeURIComponent(escape(atob(key)));
|
||||
};
|
||||
|
||||
export const apiHandler = {
|
||||
createFolder: (key, bucket) => {
|
||||
return api.post(`/buckets/${bucket}/folder`, {
|
||||
key: encode(key)
|
||||
})
|
||||
},
|
||||
deleteObject: (key, bucket) => {
|
||||
return api.post(`/buckets/${bucket}/delete`, {
|
||||
key: encode(key)
|
||||
})
|
||||
},
|
||||
downloadFile: (bucket, key, previewConfig, onDownloadProgress, abortControl) => {
|
||||
const extra = {};
|
||||
if (previewConfig.downloadType === "objectUrl" || previewConfig.downloadType === "blob") {
|
||||
extra.responseType = "arraybuffer";
|
||||
}
|
||||
if (abortControl) {
|
||||
extra.signal = abortControl.signal;
|
||||
}
|
||||
if (onDownloadProgress) {
|
||||
extra.onDownloadProgress = onDownloadProgress;
|
||||
}
|
||||
createFolder: (key, bucket) => {
|
||||
return api.post(`/buckets/${bucket}/folder`, {
|
||||
key: encode(key),
|
||||
});
|
||||
},
|
||||
deleteObject: (key, bucket) => {
|
||||
return api.post(`/buckets/${bucket}/delete`, {
|
||||
key: encode(key),
|
||||
});
|
||||
},
|
||||
downloadFile: (
|
||||
bucket,
|
||||
key,
|
||||
previewConfig,
|
||||
onDownloadProgress,
|
||||
abortControl,
|
||||
) => {
|
||||
const extra = {};
|
||||
if (
|
||||
previewConfig.downloadType === "objectUrl" ||
|
||||
previewConfig.downloadType === "blob"
|
||||
) {
|
||||
extra.responseType = "arraybuffer";
|
||||
}
|
||||
if (abortControl) {
|
||||
extra.signal = abortControl.signal;
|
||||
}
|
||||
if (onDownloadProgress) {
|
||||
extra.onDownloadProgress = onDownloadProgress;
|
||||
}
|
||||
|
||||
return api.get(
|
||||
`/buckets/${bucket}/${encode(key)}`,
|
||||
extra
|
||||
);
|
||||
},
|
||||
headFile: async (bucket, key) => {
|
||||
let prefix = ''
|
||||
if (key.includes('/')) {
|
||||
prefix = key.replace(key.split('/').pop(), '')
|
||||
}
|
||||
return api.get(`/buckets/${bucket}/${encode(key)}`, extra);
|
||||
},
|
||||
headFile: async (bucket, key) => {
|
||||
let prefix = "";
|
||||
if (key.includes("/")) {
|
||||
prefix = key.replace(key.split("/").pop(), "");
|
||||
}
|
||||
|
||||
const resp = await api.get(`/buckets/${bucket}/${encode(key)}/head`);
|
||||
const resp = await api.get(`/buckets/${bucket}/${encode(key)}/head`);
|
||||
|
||||
if (resp.status === 200) {
|
||||
return mapFile(resp.data, prefix)
|
||||
}
|
||||
},
|
||||
renameObject: (bucket, oldKey, newKey) => {
|
||||
return api.post(`/buckets/${bucket}/move`, {
|
||||
oldKey: encode(oldKey),
|
||||
newKey: encode(newKey)
|
||||
})
|
||||
},
|
||||
updateMetadata: async (bucket, key, metadata) => {
|
||||
let prefix = ''
|
||||
if (key.includes('/')) {
|
||||
prefix = key.replace(key.split('/').pop(), '')
|
||||
}
|
||||
if (resp.status === 200) {
|
||||
return mapFile(resp.data, prefix);
|
||||
}
|
||||
},
|
||||
renameObject: (bucket, oldKey, newKey) => {
|
||||
return api.post(`/buckets/${bucket}/move`, {
|
||||
oldKey: encode(oldKey),
|
||||
newKey: encode(newKey),
|
||||
});
|
||||
},
|
||||
updateMetadata: async (bucket, key, metadata) => {
|
||||
let prefix = "";
|
||||
if (key.includes("/")) {
|
||||
prefix = key.replace(key.split("/").pop(), "");
|
||||
}
|
||||
|
||||
const resp = await api.post(
|
||||
`/buckets/${bucket}/${encode(key)}`,
|
||||
{
|
||||
customMetadata: metadata
|
||||
}
|
||||
)
|
||||
const resp = await api.post(`/buckets/${bucket}/${encode(key)}`, {
|
||||
customMetadata: metadata,
|
||||
});
|
||||
|
||||
if (resp.status === 200) {
|
||||
return mapFile(resp.data, prefix)
|
||||
}
|
||||
},
|
||||
multipartCreate: (file, key, bucket) => {
|
||||
return api.post(`/buckets/${bucket}/multipart/create`, null, {
|
||||
params: {
|
||||
key: encode(key),
|
||||
httpMetadata: encode(JSON.stringify({
|
||||
contentType: file.type
|
||||
}))
|
||||
}
|
||||
})
|
||||
},
|
||||
multipartComplete: (file, key, bucket, parts, uploadId) => {
|
||||
return api.post(`/buckets/${bucket}/multipart/complete`, {
|
||||
key: encode(key),
|
||||
uploadId,
|
||||
parts
|
||||
})
|
||||
},
|
||||
multipartUpload: (uploadId, partNumber, bucket, key, chunk, callback) => {
|
||||
return api.post(`/buckets/${bucket}/multipart/upload`, chunk, {
|
||||
params: {
|
||||
key: encode(key),
|
||||
uploadId,
|
||||
partNumber
|
||||
},
|
||||
onUploadProgress: callback,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadObjects: (file, key, bucket, callback) => {
|
||||
// console.log(key)
|
||||
return api.post(`/buckets/${bucket}/upload`, file, {
|
||||
params: {
|
||||
key: encode(key),
|
||||
httpMetadata: encode(JSON.stringify({
|
||||
contentType: file.type
|
||||
}))
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress: callback
|
||||
})
|
||||
},
|
||||
listObjects: async (bucket, prefix, delimiter = '/', cursor = null) => {
|
||||
return await api.get(`/buckets/${bucket}?include=customMetadata&include=httpMetadata`, {
|
||||
params: {
|
||||
delimiter: delimiter,
|
||||
prefix: prefix && prefix !== '/' ? encode(prefix) : '',
|
||||
cursor: cursor
|
||||
}
|
||||
})
|
||||
},
|
||||
fetchFile: async (bucket, prefix, delimiter = '/') => {
|
||||
const mainStore = useMainStore();
|
||||
let truncated = true
|
||||
let cursor = null
|
||||
let contentFiles = []
|
||||
let contentFolders = []
|
||||
if (resp.status === 200) {
|
||||
return mapFile(resp.data, prefix);
|
||||
}
|
||||
},
|
||||
multipartCreate: (file, key, bucket) => {
|
||||
return api.post(`/buckets/${bucket}/multipart/create`, null, {
|
||||
params: {
|
||||
key: encode(key),
|
||||
httpMetadata: encode(
|
||||
JSON.stringify({
|
||||
contentType: file.type,
|
||||
}),
|
||||
),
|
||||
},
|
||||
});
|
||||
},
|
||||
multipartComplete: (file, key, bucket, parts, uploadId) => {
|
||||
return api.post(`/buckets/${bucket}/multipart/complete`, {
|
||||
key: encode(key),
|
||||
uploadId,
|
||||
parts,
|
||||
});
|
||||
},
|
||||
multipartUpload: (uploadId, partNumber, bucket, key, chunk, callback) => {
|
||||
return api.post(`/buckets/${bucket}/multipart/upload`, chunk, {
|
||||
params: {
|
||||
key: encode(key),
|
||||
uploadId,
|
||||
partNumber,
|
||||
},
|
||||
onUploadProgress: callback,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
},
|
||||
uploadObjects: (file, key, bucket, callback) => {
|
||||
// console.log(key)
|
||||
return api.post(`/buckets/${bucket}/upload`, file, {
|
||||
params: {
|
||||
key: encode(key),
|
||||
httpMetadata: encode(
|
||||
JSON.stringify({
|
||||
contentType: file.type,
|
||||
}),
|
||||
),
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: callback,
|
||||
});
|
||||
},
|
||||
listObjects: async (bucket, prefix, delimiter = "/", cursor = null) => {
|
||||
return await api.get(
|
||||
`/buckets/${bucket}?include=customMetadata&include=httpMetadata`,
|
||||
{
|
||||
params: {
|
||||
delimiter: delimiter,
|
||||
prefix: prefix && prefix !== "/" ? encode(prefix) : "",
|
||||
cursor: cursor,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
fetchFile: async (bucket, prefix, delimiter = "/") => {
|
||||
const mainStore = useMainStore();
|
||||
let truncated = true;
|
||||
let cursor = null;
|
||||
const contentFiles = [];
|
||||
const contentFolders = [];
|
||||
|
||||
while (truncated) {
|
||||
const response = await apiHandler.listObjects(bucket, prefix, delimiter, cursor)
|
||||
while (truncated) {
|
||||
const response = await apiHandler.listObjects(
|
||||
bucket,
|
||||
prefix,
|
||||
delimiter,
|
||||
cursor,
|
||||
);
|
||||
|
||||
truncated = response.data.truncated
|
||||
cursor = response.data.cursor
|
||||
truncated = response.data.truncated;
|
||||
cursor = response.data.cursor;
|
||||
|
||||
if (response.data.objects) {
|
||||
const files = response.data.objects.filter(function(obj) {
|
||||
return !(obj.key.endsWith('/') && delimiter !== '') && obj.key !== prefix // Remove selected folder when delimiter is defined
|
||||
}).map((obj) => mapFile(obj, prefix)).filter(obj => {
|
||||
// Remove hidden files
|
||||
return !(mainStore.showHiddenFiles !== true && obj.name.startsWith('.'))
|
||||
})
|
||||
if (response.data.objects) {
|
||||
const files = response.data.objects
|
||||
.filter((obj) => {
|
||||
return (
|
||||
!(obj.key.endsWith("/") && delimiter !== "") && obj.key !== prefix
|
||||
); // Remove selected folder when delimiter is defined
|
||||
})
|
||||
.map((obj) => mapFile(obj, prefix))
|
||||
.filter((obj) => {
|
||||
// Remove hidden files
|
||||
return !(
|
||||
mainStore.showHiddenFiles !== true && obj.name.startsWith(".")
|
||||
);
|
||||
});
|
||||
|
||||
for (const f of files) {
|
||||
contentFiles.push(f)
|
||||
}
|
||||
}
|
||||
for (const f of files) {
|
||||
contentFiles.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.data.delimitedPrefixes) {
|
||||
const folders = response.data.delimitedPrefixes.map(function (obj) {
|
||||
return {
|
||||
name: obj.replace(prefix, ''),
|
||||
hash: encode(obj.key),
|
||||
key: obj,
|
||||
lastModified: '--',
|
||||
timestamp: 0,
|
||||
size: '--',
|
||||
sizeRaw: 0,
|
||||
type: 'folder',
|
||||
icon: 'folder',
|
||||
color: 'orange',
|
||||
}
|
||||
}).filter(obj => {
|
||||
// Remove hidden files
|
||||
return !(mainStore.showHiddenFiles !== true && obj.name.startsWith('.'))
|
||||
})
|
||||
if (response.data.delimitedPrefixes) {
|
||||
const folders = response.data.delimitedPrefixes
|
||||
.map((obj) => ({
|
||||
name: obj.replace(prefix, ""),
|
||||
hash: encode(obj.key),
|
||||
key: obj,
|
||||
lastModified: "--",
|
||||
timestamp: 0,
|
||||
size: "--",
|
||||
sizeRaw: 0,
|
||||
type: "folder",
|
||||
icon: "folder",
|
||||
color: "orange",
|
||||
}))
|
||||
.filter((obj) => {
|
||||
// Remove hidden files
|
||||
return !(
|
||||
mainStore.showHiddenFiles !== true && obj.name.startsWith(".")
|
||||
);
|
||||
});
|
||||
|
||||
for (const f of folders) {
|
||||
contentFolders.push(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const f of folders) {
|
||||
contentFolders.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
...contentFolders,
|
||||
...contentFiles
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...contentFolders, ...contentFiles];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { boot } from 'quasar/wrappers';
|
||||
import { useAuthStore } from 'stores/auth-store';
|
||||
import { boot } from "quasar/wrappers";
|
||||
import { useAuthStore } from "stores/auth-store";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
|
||||
export default boot(async ({router, store}) => {
|
||||
// Check if theres any auth token stored, if there is, try to fetch or redirect
|
||||
const authStore = useAuthStore(store);
|
||||
const authResp = await authStore.CheckLoginInStorage(router);
|
||||
export default boot(async ({ router, store }) => {
|
||||
// Check if theres any auth token stored, if there is, try to fetch or redirect
|
||||
const authStore = useAuthStore(store);
|
||||
const authResp = await authStore.CheckLoginInStorage(router);
|
||||
|
||||
if (authResp === false) { // No auth token stored, try to fetch without auth or redirect
|
||||
const mainStore = useMainStore(store)
|
||||
await mainStore.loadServerConfigs(router, true)
|
||||
}
|
||||
if (authResp === false) {
|
||||
// No auth token stored, try to fetch without auth or redirect
|
||||
const mainStore = useMainStore(store);
|
||||
await mainStore.loadServerConfigs(router, true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { boot } from 'quasar/wrappers'
|
||||
import axios from 'axios'
|
||||
import axios from "axios";
|
||||
import { boot } from "quasar/wrappers";
|
||||
|
||||
// Be careful when using SSR for cross-request state pollution
|
||||
// due to creating a Singleton instance here;
|
||||
@@ -7,22 +7,22 @@ import axios from 'axios'
|
||||
// good idea to move this instance creation inside of the
|
||||
// "export default () => {}" function below (which runs individually
|
||||
// for each client)
|
||||
let url = window.location.origin
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
url = process.env.VUE_APP_SERVER_URL || 'http://localhost:8787'
|
||||
let url = window.location.origin;
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
url = process.env.VUE_APP_SERVER_URL || "http://localhost:8787";
|
||||
}
|
||||
const api = axios.create({ baseURL: `${url}/api` })
|
||||
const api = axios.create({ baseURL: `${url}/api` });
|
||||
|
||||
export default boot(({ app }) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
||||
app.config.globalProperties.$axios = axios
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
app.config.globalProperties.$axios = axios;
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
})
|
||||
app.config.globalProperties.$api = api;
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
});
|
||||
|
||||
export { api }
|
||||
export { api };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EventBus } from 'quasar'
|
||||
import { boot } from 'quasar/wrappers'
|
||||
import { EventBus } from "quasar";
|
||||
import { boot } from "quasar/wrappers";
|
||||
|
||||
export default boot(({ app }) => {
|
||||
const bus = new EventBus()
|
||||
const bus = new EventBus();
|
||||
|
||||
// for Options API
|
||||
app.config.globalProperties.$bus = bus
|
||||
// for Options API
|
||||
app.config.globalProperties.$bus = bus;
|
||||
|
||||
// for Composition API
|
||||
app.provide('bus', bus)
|
||||
})
|
||||
// for Composition API
|
||||
app.provide("bus", bus);
|
||||
});
|
||||
|
||||
@@ -24,68 +24,73 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { ROOT_FOLDER, apiHandler, decode, encode } from "src/appUtils";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { apiHandler, decode, encode, ROOT_FOLDER } from "src/appUtils";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CreateFile",
|
||||
data: function() {
|
||||
return {
|
||||
modal: false,
|
||||
newFileName: "",
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
cancel: function() {
|
||||
this.modal = false;
|
||||
this.newFileName = "";
|
||||
this.loading = false
|
||||
},
|
||||
onSubmit: async function() {
|
||||
this.loading = true
|
||||
name: "CreateFile",
|
||||
data: () => ({
|
||||
modal: false,
|
||||
newFileName: "",
|
||||
loading: false,
|
||||
}),
|
||||
methods: {
|
||||
cancel: function () {
|
||||
this.modal = false;
|
||||
this.newFileName = "";
|
||||
this.loading = false;
|
||||
},
|
||||
onSubmit: async function () {
|
||||
this.loading = true;
|
||||
|
||||
const newFile = new Blob([''], {type: 'text/plain'});
|
||||
await apiHandler.uploadObjects(newFile, this.selectedFolder + this.newFileName, this.selectedBucket);
|
||||
const newFile = new Blob([""], { type: "text/plain" });
|
||||
await apiHandler.uploadObjects(
|
||||
newFile,
|
||||
this.selectedFolder + this.newFileName,
|
||||
this.selectedBucket,
|
||||
);
|
||||
|
||||
this.$bus.emit("fetchFiles");
|
||||
this.$bus.emit("fetchFiles");
|
||||
|
||||
// TODO: open the new file in preview and edit mode
|
||||
// await this.$router.push({
|
||||
// name: "files-file",
|
||||
// params: {
|
||||
// bucket: this.$route.params.bucket,
|
||||
// folder: this.$route.params.folder || ROOT_FOLDER,
|
||||
// file: encode(this.newFileName)
|
||||
// }
|
||||
// });
|
||||
// TODO: open the new file in preview and edit mode
|
||||
// await this.$router.push({
|
||||
// name: "files-file",
|
||||
// params: {
|
||||
// bucket: this.$route.params.bucket,
|
||||
// folder: this.$route.params.folder || ROOT_FOLDER,
|
||||
// file: encode(this.newFileName)
|
||||
// }
|
||||
// });
|
||||
|
||||
this.loading = false
|
||||
this.modal = false;
|
||||
this.newFileName = "";
|
||||
},
|
||||
open: function() {
|
||||
this.modal = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function() {
|
||||
if (this.$route.params.folder && this.$route.params.folder !== ROOT_FOLDER) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
this.loading = false;
|
||||
this.modal = false;
|
||||
this.newFileName = "";
|
||||
},
|
||||
open: function () {
|
||||
this.modal = true;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (
|
||||
this.$route.params.folder &&
|
||||
this.$route.params.folder !== ROOT_FOLDER
|
||||
) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
return {
|
||||
mainStore
|
||||
};
|
||||
}
|
||||
return {
|
||||
mainStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -24,54 +24,58 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { ROOT_FOLDER, apiHandler, decode } from "src/appUtils";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { apiHandler, decode, ROOT_FOLDER } from "src/appUtils";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CreateFolder",
|
||||
data: function() {
|
||||
return {
|
||||
modal: false,
|
||||
newFolderName: "",
|
||||
loading: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
cancel: function() {
|
||||
this.modal = false;
|
||||
this.newFolderName = "";
|
||||
this.loading = false
|
||||
},
|
||||
onSubmit: async function() {
|
||||
this.loading = true
|
||||
await apiHandler.createFolder(this.selectedFolder + this.newFolderName + "/", this.selectedBucket);
|
||||
this.$bus.emit("fetchFiles");
|
||||
this.loading = false
|
||||
this.modal = false;
|
||||
this.newFolderName = "";
|
||||
},
|
||||
open: function() {
|
||||
this.modal = true;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function() {
|
||||
if (this.$route.params.folder && this.$route.params.folder !== ROOT_FOLDER) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
name: "CreateFolder",
|
||||
data: () => ({
|
||||
modal: false,
|
||||
newFolderName: "",
|
||||
loading: false,
|
||||
}),
|
||||
methods: {
|
||||
cancel: function () {
|
||||
this.modal = false;
|
||||
this.newFolderName = "";
|
||||
this.loading = false;
|
||||
},
|
||||
onSubmit: async function () {
|
||||
this.loading = true;
|
||||
await apiHandler.createFolder(
|
||||
`${this.selectedFolder + this.newFolderName}/`,
|
||||
this.selectedBucket,
|
||||
);
|
||||
this.$bus.emit("fetchFiles");
|
||||
this.loading = false;
|
||||
this.modal = false;
|
||||
this.newFolderName = "";
|
||||
},
|
||||
open: function () {
|
||||
this.modal = true;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (
|
||||
this.$route.params.folder &&
|
||||
this.$route.params.folder !== ROOT_FOLDER
|
||||
) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
return {
|
||||
mainStore
|
||||
};
|
||||
}
|
||||
return {
|
||||
mainStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -33,144 +33,156 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { apiHandler, decode, encode, ROOT_FOLDER } from "src/appUtils";
|
||||
import { useQuasar } from "quasar";
|
||||
import { ROOT_FOLDER, apiHandler, decode, encode } from "src/appUtils";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FileOptions',
|
||||
data: function () {
|
||||
return {
|
||||
row: null,
|
||||
deleteFolderContents: [],
|
||||
deleteModal: false,
|
||||
renameModal: false,
|
||||
deleteFolderInnerFilesCount: null,
|
||||
newFolderName: '',
|
||||
renameInput: '',
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteObject: async function(row) {
|
||||
this.deleteModal = true
|
||||
this.row = row
|
||||
if (row.type === 'folder') {
|
||||
this.deleteFolderContents = await apiHandler.fetchFile(this.selectedBucket, row.key, '')
|
||||
this.deleteFolderInnerFilesCount = this.deleteFolderContents.length
|
||||
}
|
||||
},
|
||||
renameObject: async function(row) {
|
||||
this.renameModal = true
|
||||
this.row = row
|
||||
// console.log(row)
|
||||
this.renameInput = row.name
|
||||
},
|
||||
renameConfirm: async function() {
|
||||
if (this.renameInput.length === 0) {
|
||||
return
|
||||
}
|
||||
name: "FileOptions",
|
||||
data: () => ({
|
||||
row: null,
|
||||
deleteFolderContents: [],
|
||||
deleteModal: false,
|
||||
renameModal: false,
|
||||
deleteFolderInnerFilesCount: null,
|
||||
newFolderName: "",
|
||||
renameInput: "",
|
||||
loading: false,
|
||||
}),
|
||||
methods: {
|
||||
deleteObject: async function (row) {
|
||||
this.deleteModal = true;
|
||||
this.row = row;
|
||||
if (row.type === "folder") {
|
||||
this.deleteFolderContents = await apiHandler.fetchFile(
|
||||
this.selectedBucket,
|
||||
row.key,
|
||||
"",
|
||||
);
|
||||
this.deleteFolderInnerFilesCount = this.deleteFolderContents.length;
|
||||
}
|
||||
},
|
||||
renameObject: async function (row) {
|
||||
this.renameModal = true;
|
||||
this.row = row;
|
||||
// console.log(row)
|
||||
this.renameInput = row.name;
|
||||
},
|
||||
renameConfirm: async function () {
|
||||
if (this.renameInput.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
await apiHandler.renameObject(this.selectedBucket, this.row.key, this.row.key.replace(this.row.name, this.renameInput))
|
||||
this.loading = true;
|
||||
await apiHandler.renameObject(
|
||||
this.selectedBucket,
|
||||
this.row.key,
|
||||
this.row.key.replace(this.row.name, this.renameInput),
|
||||
);
|
||||
|
||||
this.$bus.emit('fetchFiles')
|
||||
this.reset()
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: 'File renamed!',
|
||||
timeout: 2500 // we will timeout it in 2.5s
|
||||
})
|
||||
},
|
||||
deleteConfirm: async function() {
|
||||
if (this.row.type === 'folder') {
|
||||
// When deleting folders, first must copy the objects, because the popup close forces a reset on properties
|
||||
const originalFolder = { ...this.row }
|
||||
const folderContents = [...this.deleteFolderContents]
|
||||
const folderContentsCount = this.deleteFolderInnerFilesCount
|
||||
this.$bus.emit("fetchFiles");
|
||||
this.reset();
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: "File renamed!",
|
||||
timeout: 2500, // we will timeout it in 2.5s
|
||||
});
|
||||
},
|
||||
deleteConfirm: async function () {
|
||||
if (this.row.type === "folder") {
|
||||
// When deleting folders, first must copy the objects, because the popup close forces a reset on properties
|
||||
const originalFolder = { ...this.row };
|
||||
const folderContents = [...this.deleteFolderContents];
|
||||
const folderContentsCount = this.deleteFolderInnerFilesCount;
|
||||
|
||||
this.deleteModal = false
|
||||
this.deleteModal = false;
|
||||
|
||||
const notif = this.q.notify({
|
||||
group: false,
|
||||
spinner: true,
|
||||
message: 'Deleting files...',
|
||||
caption: '0%',
|
||||
timeout: 0
|
||||
})
|
||||
const notif = this.q.notify({
|
||||
group: false,
|
||||
spinner: true,
|
||||
message: "Deleting files...",
|
||||
caption: "0%",
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
for (const [i, innerFile] of folderContents.entries()) {
|
||||
if (innerFile.key) {
|
||||
await apiHandler.deleteObject(innerFile.key, this.selectedBucket)
|
||||
}
|
||||
notif({
|
||||
caption: `${parseInt(i*100/(folderContentsCount+1))}%` // +1 because still needs to delete the folder
|
||||
})
|
||||
}
|
||||
for (const [i, innerFile] of folderContents.entries()) {
|
||||
if (innerFile.key) {
|
||||
await apiHandler.deleteObject(innerFile.key, this.selectedBucket);
|
||||
}
|
||||
notif({
|
||||
caption: `${Number.parseInt((i * 100) / (folderContentsCount + 1))}%`, // +1 because still needs to delete the folder
|
||||
});
|
||||
}
|
||||
|
||||
await apiHandler.deleteObject(originalFolder.key, this.selectedBucket)
|
||||
await apiHandler.deleteObject(originalFolder.key, this.selectedBucket);
|
||||
|
||||
notif({
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
caption: '100%',
|
||||
message: 'Folder deleted!',
|
||||
timeout: 2500 // we will timeout it in 2.5s
|
||||
})
|
||||
} else {
|
||||
this.deleteModal = false
|
||||
await apiHandler.deleteObject(this.row.key, this.selectedBucket)
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: 'File deleted!',
|
||||
timeout: 2500 // we will timeout it in 2.5s
|
||||
})
|
||||
}
|
||||
notif({
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
caption: "100%",
|
||||
message: "Folder deleted!",
|
||||
timeout: 2500, // we will timeout it in 2.5s
|
||||
});
|
||||
} else {
|
||||
this.deleteModal = false;
|
||||
await apiHandler.deleteObject(this.row.key, this.selectedBucket);
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: "File deleted!",
|
||||
timeout: 2500, // we will timeout it in 2.5s
|
||||
});
|
||||
}
|
||||
|
||||
this.$bus.emit('fetchFiles')
|
||||
this.reset()
|
||||
},
|
||||
reset: function() {
|
||||
this.loading = false
|
||||
this.deleteModal = false
|
||||
this.renameModal = false
|
||||
this.renameInput = ''
|
||||
this.row = null
|
||||
this.deleteFolderInnerFilesCount = null
|
||||
this.deleteFolderContents = []
|
||||
},
|
||||
onSubmit: async function() {
|
||||
await apiHandler.createFolder(this.selectedFolder + this.newFolderName + '/', this.selectedBucket)
|
||||
this.$bus.emit('fetchFiles')
|
||||
this.modal = false
|
||||
},
|
||||
open: function() {
|
||||
this.modal = true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (this.$route.params.folder && this.$route.params.folder !== ROOT_FOLDER) {
|
||||
return decode(this.$route.params.folder)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar()
|
||||
};
|
||||
},
|
||||
})
|
||||
this.$bus.emit("fetchFiles");
|
||||
this.reset();
|
||||
},
|
||||
reset: function () {
|
||||
this.loading = false;
|
||||
this.deleteModal = false;
|
||||
this.renameModal = false;
|
||||
this.renameInput = "";
|
||||
this.row = null;
|
||||
this.deleteFolderInnerFilesCount = null;
|
||||
this.deleteFolderContents = [];
|
||||
},
|
||||
onSubmit: async function () {
|
||||
await apiHandler.createFolder(
|
||||
`${this.selectedFolder + this.newFolderName}/`,
|
||||
this.selectedBucket,
|
||||
);
|
||||
this.$bus.emit("fetchFiles");
|
||||
this.modal = false;
|
||||
},
|
||||
open: function () {
|
||||
this.modal = true;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (
|
||||
this.$route.params.folder &&
|
||||
this.$route.params.folder !== ROOT_FOLDER
|
||||
) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -19,30 +19,33 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BucketPicker",
|
||||
methods: {
|
||||
changeBucket: function(bucket) {
|
||||
this.$router.push({ name: `${this.selectedApp}-home`, params: { bucket: bucket } });
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedApp: function() {
|
||||
return this.$route.name.split("-")[0];
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
name: "BucketPicker",
|
||||
methods: {
|
||||
changeBucket: function (bucket) {
|
||||
this.$router.push({
|
||||
name: `${this.selectedApp}-home`,
|
||||
params: { bucket: bucket },
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedApp: function () {
|
||||
return this.$route.name.split("-")[0];
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
return {
|
||||
mainStore
|
||||
};
|
||||
}
|
||||
return {
|
||||
mainStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -114,56 +114,55 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import CreateFolder from "components/files/CreateFolder.vue";
|
||||
import CreateFile from "components/files/CreateFile.vue";
|
||||
import CreateFolder from "components/files/CreateFolder.vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LeftSidebar",
|
||||
data: function() {
|
||||
return {
|
||||
upgradePopup: false,
|
||||
settingsPopup: false
|
||||
};
|
||||
},
|
||||
components: { CreateFolder, CreateFile },
|
||||
methods: {
|
||||
gotoEmail: function() {
|
||||
if (this.selectedApp !== "email")
|
||||
this.changeApp("email");
|
||||
},
|
||||
gotoFiles: function() {
|
||||
if (this.selectedApp !== "files")
|
||||
this.changeApp("files");
|
||||
},
|
||||
changeApp: function(app) {
|
||||
this.$router.push({ name: `${app}-home`, params: { bucket: this.selectedBucket } });
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedApp: function() {
|
||||
return this.$route.name.split("-")[0];
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
const alertSeen = localStorage.getItem("DASH_V2_ALERT");
|
||||
name: "LeftSidebar",
|
||||
data: () => ({
|
||||
upgradePopup: false,
|
||||
settingsPopup: false,
|
||||
}),
|
||||
components: { CreateFolder, CreateFile },
|
||||
methods: {
|
||||
gotoEmail: function () {
|
||||
if (this.selectedApp !== "email") this.changeApp("email");
|
||||
},
|
||||
gotoFiles: function () {
|
||||
if (this.selectedApp !== "files") this.changeApp("files");
|
||||
},
|
||||
changeApp: function (app) {
|
||||
this.$router.push({
|
||||
name: `${app}-home`,
|
||||
params: { bucket: this.selectedBucket },
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedApp: function () {
|
||||
return this.$route.name.split("-")[0];
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
const alertSeen = localStorage.getItem("DASH_V2_ALERT");
|
||||
|
||||
if (!alertSeen) {
|
||||
this.upgradePopup = true;
|
||||
localStorage.setItem("DASH_V2_ALERT", true);
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
if (!alertSeen) {
|
||||
this.upgradePopup = true;
|
||||
localStorage.setItem("DASH_V2_ALERT", true);
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
return {
|
||||
mainStore
|
||||
};
|
||||
}
|
||||
return {
|
||||
mainStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,161 +19,177 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RightSidebar',
|
||||
data: function () {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
label: 'Dessert (100g serving)',
|
||||
align: 'left',
|
||||
field: row => row.name,
|
||||
format: val => `${val}`,
|
||||
sortable: true
|
||||
},
|
||||
{ name: 'calories', align: 'center', label: 'Calories', field: 'calories', sortable: true },
|
||||
{ name: 'fat', label: 'Fat (g)', field: 'fat', sortable: true },
|
||||
{ name: 'carbs', label: 'Carbs (g)', field: 'carbs' },
|
||||
{ name: 'protein', label: 'Protein (g)', field: 'protein' },
|
||||
{ name: 'sodium', label: 'Sodium (mg)', field: 'sodium' },
|
||||
{ name: 'calcium', label: 'Calcium (%)', field: 'calcium', sortable: true, sort: (a, b) => parseInt(a, 10) - parseInt(b, 10) },
|
||||
{ name: 'iron', label: 'Iron (%)', field: 'iron', sortable: true, sort: (a, b) => parseInt(a, 10) - parseInt(b, 10) }
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: 'Frozen Yogurt',
|
||||
calories: 159,
|
||||
fat: 6.0,
|
||||
carbs: 24,
|
||||
protein: 4.0,
|
||||
sodium: 87,
|
||||
calcium: '14%',
|
||||
iron: '1%'
|
||||
},
|
||||
{
|
||||
name: 'Ice cream sandwich',
|
||||
calories: 237,
|
||||
fat: 9.0,
|
||||
carbs: 37,
|
||||
protein: 4.3,
|
||||
sodium: 129,
|
||||
calcium: '8%',
|
||||
iron: '1%'
|
||||
},
|
||||
{
|
||||
name: 'Eclair',
|
||||
calories: 262,
|
||||
fat: 16.0,
|
||||
carbs: 23,
|
||||
protein: 6.0,
|
||||
sodium: 337,
|
||||
calcium: '6%',
|
||||
iron: '7%'
|
||||
},
|
||||
{
|
||||
name: 'Cupcake',
|
||||
calories: 305,
|
||||
fat: 3.7,
|
||||
carbs: 67,
|
||||
protein: 4.3,
|
||||
sodium: 413,
|
||||
calcium: '3%',
|
||||
iron: '8%'
|
||||
},
|
||||
{
|
||||
name: 'Gingerbread',
|
||||
calories: 356,
|
||||
fat: 16.0,
|
||||
carbs: 49,
|
||||
protein: 3.9,
|
||||
sodium: 327,
|
||||
calcium: '7%',
|
||||
iron: '16%'
|
||||
},
|
||||
{
|
||||
name: 'Jelly bean',
|
||||
calories: 375,
|
||||
fat: 0.0,
|
||||
carbs: 94,
|
||||
protein: 0.0,
|
||||
sodium: 50,
|
||||
calcium: '0%',
|
||||
iron: '0%'
|
||||
},
|
||||
{
|
||||
name: 'Lollipop',
|
||||
calories: 392,
|
||||
fat: 0.2,
|
||||
carbs: 98,
|
||||
protein: 0,
|
||||
sodium: 38,
|
||||
calcium: '0%',
|
||||
iron: '2%'
|
||||
},
|
||||
{
|
||||
name: 'Honeycomb',
|
||||
calories: 408,
|
||||
fat: 3.2,
|
||||
carbs: 87,
|
||||
protein: 6.5,
|
||||
sodium: 562,
|
||||
calcium: '0%',
|
||||
iron: '45%'
|
||||
},
|
||||
{
|
||||
name: 'Donut',
|
||||
calories: 452,
|
||||
fat: 25.0,
|
||||
carbs: 51,
|
||||
protein: 4.9,
|
||||
sodium: 326,
|
||||
calcium: '2%',
|
||||
iron: '22%'
|
||||
},
|
||||
{
|
||||
name: 'KitKat',
|
||||
calories: 518,
|
||||
fat: 26.0,
|
||||
carbs: 65,
|
||||
protein: 7,
|
||||
sodium: 54,
|
||||
calcium: '12%',
|
||||
iron: '6%'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openFileDetails: function(row) {
|
||||
// this.$emit('updateDrawer', true) // TODO: enable
|
||||
// console.log(row)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket
|
||||
},
|
||||
selectedApp: function () {
|
||||
return this.$route.name.split('-')[0]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$bus.on('openFileDetails', this.openFileDetails)
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$bus.off('openFileDetails')
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
name: "RightSidebar",
|
||||
data: () => ({
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
required: true,
|
||||
label: "Dessert (100g serving)",
|
||||
align: "left",
|
||||
field: (row) => row.name,
|
||||
format: (val) => `${val}`,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: "calories",
|
||||
align: "center",
|
||||
label: "Calories",
|
||||
field: "calories",
|
||||
sortable: true,
|
||||
},
|
||||
{ name: "fat", label: "Fat (g)", field: "fat", sortable: true },
|
||||
{ name: "carbs", label: "Carbs (g)", field: "carbs" },
|
||||
{ name: "protein", label: "Protein (g)", field: "protein" },
|
||||
{ name: "sodium", label: "Sodium (mg)", field: "sodium" },
|
||||
{
|
||||
name: "calcium",
|
||||
label: "Calcium (%)",
|
||||
field: "calcium",
|
||||
sortable: true,
|
||||
sort: (a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10),
|
||||
},
|
||||
{
|
||||
name: "iron",
|
||||
label: "Iron (%)",
|
||||
field: "iron",
|
||||
sortable: true,
|
||||
sort: (a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10),
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
name: "Frozen Yogurt",
|
||||
calories: 159,
|
||||
fat: 6.0,
|
||||
carbs: 24,
|
||||
protein: 4.0,
|
||||
sodium: 87,
|
||||
calcium: "14%",
|
||||
iron: "1%",
|
||||
},
|
||||
{
|
||||
name: "Ice cream sandwich",
|
||||
calories: 237,
|
||||
fat: 9.0,
|
||||
carbs: 37,
|
||||
protein: 4.3,
|
||||
sodium: 129,
|
||||
calcium: "8%",
|
||||
iron: "1%",
|
||||
},
|
||||
{
|
||||
name: "Eclair",
|
||||
calories: 262,
|
||||
fat: 16.0,
|
||||
carbs: 23,
|
||||
protein: 6.0,
|
||||
sodium: 337,
|
||||
calcium: "6%",
|
||||
iron: "7%",
|
||||
},
|
||||
{
|
||||
name: "Cupcake",
|
||||
calories: 305,
|
||||
fat: 3.7,
|
||||
carbs: 67,
|
||||
protein: 4.3,
|
||||
sodium: 413,
|
||||
calcium: "3%",
|
||||
iron: "8%",
|
||||
},
|
||||
{
|
||||
name: "Gingerbread",
|
||||
calories: 356,
|
||||
fat: 16.0,
|
||||
carbs: 49,
|
||||
protein: 3.9,
|
||||
sodium: 327,
|
||||
calcium: "7%",
|
||||
iron: "16%",
|
||||
},
|
||||
{
|
||||
name: "Jelly bean",
|
||||
calories: 375,
|
||||
fat: 0.0,
|
||||
carbs: 94,
|
||||
protein: 0.0,
|
||||
sodium: 50,
|
||||
calcium: "0%",
|
||||
iron: "0%",
|
||||
},
|
||||
{
|
||||
name: "Lollipop",
|
||||
calories: 392,
|
||||
fat: 0.2,
|
||||
carbs: 98,
|
||||
protein: 0,
|
||||
sodium: 38,
|
||||
calcium: "0%",
|
||||
iron: "2%",
|
||||
},
|
||||
{
|
||||
name: "Honeycomb",
|
||||
calories: 408,
|
||||
fat: 3.2,
|
||||
carbs: 87,
|
||||
protein: 6.5,
|
||||
sodium: 562,
|
||||
calcium: "0%",
|
||||
iron: "45%",
|
||||
},
|
||||
{
|
||||
name: "Donut",
|
||||
calories: 452,
|
||||
fat: 25.0,
|
||||
carbs: 51,
|
||||
protein: 4.9,
|
||||
sodium: 326,
|
||||
calcium: "2%",
|
||||
iron: "22%",
|
||||
},
|
||||
{
|
||||
name: "KitKat",
|
||||
calories: 518,
|
||||
fat: 26.0,
|
||||
carbs: 65,
|
||||
protein: 7,
|
||||
sodium: 54,
|
||||
calcium: "12%",
|
||||
iron: "6%",
|
||||
},
|
||||
],
|
||||
}),
|
||||
methods: {
|
||||
openFileDetails: (row) => {
|
||||
// this.$emit('updateDrawer', true) // TODO: enable
|
||||
// console.log(row)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedApp: function () {
|
||||
return this.$route.name.split("-")[0];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$bus.on("openFileDetails", this.openFileDetails);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$bus.off("openFileDetails");
|
||||
},
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
|
||||
return {
|
||||
mainStore,
|
||||
};
|
||||
},
|
||||
})
|
||||
return {
|
||||
mainStore,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import BucketPicker from "components/main/BucketPicker.vue";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TopBar",
|
||||
emits: ['toggle'],
|
||||
components: { BucketPicker }
|
||||
name: "TopBar",
|
||||
emits: ["toggle"],
|
||||
components: { BucketPicker },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -56,33 +56,31 @@
|
||||
|
||||
|
||||
<script>
|
||||
import PostalMime from "postal-mime";
|
||||
import { convert } from "html-to-text";
|
||||
import PostalMime from "postal-mime";
|
||||
|
||||
export default {
|
||||
props: ['filedata'],
|
||||
data: function () {
|
||||
return {
|
||||
emailData: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async parseEmail() {
|
||||
try {
|
||||
const parser = new PostalMime.default();
|
||||
const parsedEmail = await parser.parse(this.filedata);
|
||||
props: ["filedata"],
|
||||
data: () => ({
|
||||
emailData: null,
|
||||
}),
|
||||
methods: {
|
||||
async parseEmail() {
|
||||
try {
|
||||
const parser = new PostalMime.default();
|
||||
const parsedEmail = await parser.parse(this.filedata);
|
||||
|
||||
this.emailData = parsedEmail;
|
||||
if (parsedEmail.html) {
|
||||
this.emailData.htmlAsText = convert(parsedEmail.html, {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing email data:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.parseEmail();
|
||||
}
|
||||
}
|
||||
this.emailData = parsedEmail;
|
||||
if (parsedEmail.html) {
|
||||
this.emailData.htmlAsText = convert(parsedEmail.html, {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing email data:", error);
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.parseEmail();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -129,310 +129,321 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EmailViewer from "components/preview/EmailViewer.vue";
|
||||
import PdfViewer from "components/preview/PdfViewer.vue";
|
||||
import LogGz from "components/preview/logGz.vue";
|
||||
import EmailViewer from "components/preview/EmailViewer.vue";
|
||||
import { parseMarkdown } from "src/parsers/markdown";
|
||||
import { apiHandler, bytesToMegabytes, decode, ROOT_FOLDER } from "src/appUtils";
|
||||
import { useQuasar } from "quasar";
|
||||
import {
|
||||
ROOT_FOLDER,
|
||||
apiHandler,
|
||||
bytesToMegabytes,
|
||||
decode,
|
||||
} from "src/appUtils";
|
||||
import { parseMarkdown } from "src/parsers/markdown";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LogGz,
|
||||
PdfViewer,
|
||||
EmailViewer
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
open: false,
|
||||
editMode: false,
|
||||
components: {
|
||||
LogGz,
|
||||
PdfViewer,
|
||||
EmailViewer,
|
||||
},
|
||||
data: () => ({
|
||||
open: false,
|
||||
editMode: false,
|
||||
|
||||
downloadProgress: 0,
|
||||
abortControl: undefined,
|
||||
type: undefined,
|
||||
file: undefined,
|
||||
filename: undefined,
|
||||
fileData: undefined,
|
||||
fileDataEdited: undefined,
|
||||
downloadProgress: 0,
|
||||
abortControl: undefined,
|
||||
type: undefined,
|
||||
file: undefined,
|
||||
filename: undefined,
|
||||
fileData: undefined,
|
||||
fileDataEdited: undefined,
|
||||
|
||||
previewConfig: [
|
||||
{
|
||||
extensions: ["png", "jpg", "jpeg", "webp", "avif"],
|
||||
type: "image",
|
||||
downloadType: "objectUrl"
|
||||
},
|
||||
{
|
||||
extensions: ["mp3"],
|
||||
type: "audio",
|
||||
downloadType: "objectUrl"
|
||||
},
|
||||
{
|
||||
extensions: ["mp4", "ogg"],
|
||||
type: "video",
|
||||
downloadType: "objectUrl"
|
||||
},
|
||||
{
|
||||
extensions: ["pdf"],
|
||||
type: "pdf",
|
||||
downloadType: "objectUrl"
|
||||
},
|
||||
{
|
||||
extensions: ["txt"],
|
||||
type: "text",
|
||||
downloadType: "text"
|
||||
},
|
||||
{
|
||||
extensions: ["md"],
|
||||
type: "markdown",
|
||||
downloadType: "text"
|
||||
},
|
||||
{
|
||||
extensions: ["csv"],
|
||||
type: "csv",
|
||||
downloadType: "text"
|
||||
},
|
||||
{
|
||||
extensions: ["json"],
|
||||
type: "json",
|
||||
downloadType: "text"
|
||||
},
|
||||
{
|
||||
extensions: ["html"],
|
||||
type: "html",
|
||||
downloadType: "text"
|
||||
},
|
||||
{
|
||||
extensions: ["log.gz"],
|
||||
type: "logs",
|
||||
downloadType: "blob"
|
||||
},
|
||||
{
|
||||
extensions: ["eml"],
|
||||
type: "email",
|
||||
downloadType: "text"
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getType(filename) {
|
||||
for (const config of this.previewConfig) {
|
||||
for (const extension of config.extensions) {
|
||||
if (filename.endsWith(extension)) {
|
||||
return { type: config.type, downloadType: config.downloadType };
|
||||
}
|
||||
}
|
||||
}
|
||||
previewConfig: [
|
||||
{
|
||||
extensions: ["png", "jpg", "jpeg", "webp", "avif"],
|
||||
type: "image",
|
||||
downloadType: "objectUrl",
|
||||
},
|
||||
{
|
||||
extensions: ["mp3"],
|
||||
type: "audio",
|
||||
downloadType: "objectUrl",
|
||||
},
|
||||
{
|
||||
extensions: ["mp4", "ogg"],
|
||||
type: "video",
|
||||
downloadType: "objectUrl",
|
||||
},
|
||||
{
|
||||
extensions: ["pdf"],
|
||||
type: "pdf",
|
||||
downloadType: "objectUrl",
|
||||
},
|
||||
{
|
||||
extensions: ["txt"],
|
||||
type: "text",
|
||||
downloadType: "text",
|
||||
},
|
||||
{
|
||||
extensions: ["md"],
|
||||
type: "markdown",
|
||||
downloadType: "text",
|
||||
},
|
||||
{
|
||||
extensions: ["csv"],
|
||||
type: "csv",
|
||||
downloadType: "text",
|
||||
},
|
||||
{
|
||||
extensions: ["json"],
|
||||
type: "json",
|
||||
downloadType: "text",
|
||||
},
|
||||
{
|
||||
extensions: ["html"],
|
||||
type: "html",
|
||||
downloadType: "text",
|
||||
},
|
||||
{
|
||||
extensions: ["log.gz"],
|
||||
type: "logs",
|
||||
downloadType: "blob",
|
||||
},
|
||||
{
|
||||
extensions: ["eml"],
|
||||
type: "email",
|
||||
downloadType: "text",
|
||||
},
|
||||
],
|
||||
}),
|
||||
methods: {
|
||||
getType(filename) {
|
||||
for (const config of this.previewConfig) {
|
||||
for (const extension of config.extensions) {
|
||||
if (filename.endsWith(extension)) {
|
||||
return { type: config.type, downloadType: config.downloadType };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open unknown files as text
|
||||
return {
|
||||
type: "unknown",
|
||||
downloadType: "text"
|
||||
}
|
||||
},
|
||||
async openFile(file) {
|
||||
if (bytesToMegabytes(file.size) > 200) {
|
||||
this.q.notify({
|
||||
message: "File is too big to preview.",
|
||||
color: "orange"
|
||||
});
|
||||
// Open unknown files as text
|
||||
return {
|
||||
type: "unknown",
|
||||
downloadType: "text",
|
||||
};
|
||||
},
|
||||
async openFile(file) {
|
||||
if (bytesToMegabytes(file.size) > 200) {
|
||||
this.q.notify({
|
||||
message: "File is too big to preview.",
|
||||
color: "orange",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const previewConfig = this.getType(file.name);
|
||||
// if (previewConfig === undefined) {
|
||||
// this.q.notify({
|
||||
// message: "File preview is not supported.",
|
||||
// color: "orange"
|
||||
// });
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
const previewConfig = this.getType(file.name);
|
||||
// if (previewConfig === undefined) {
|
||||
// this.q.notify({
|
||||
// message: "File preview is not supported.",
|
||||
// color: "orange"
|
||||
// });
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.abortControl = new AbortController();
|
||||
this.abortControl = new AbortController();
|
||||
|
||||
await this.$router.push({
|
||||
name: "files-file",
|
||||
params: {
|
||||
bucket: this.$route.params.bucket,
|
||||
folder: this.$route.params.folder || ROOT_FOLDER,
|
||||
file: file.nameHash
|
||||
}
|
||||
});
|
||||
await this.$router.push({
|
||||
name: "files-file",
|
||||
params: {
|
||||
bucket: this.$route.params.bucket,
|
||||
folder: this.$route.params.folder || ROOT_FOLDER,
|
||||
file: file.nameHash,
|
||||
},
|
||||
});
|
||||
|
||||
// This needs to be set before download to open the modal
|
||||
this.filename = file.name;
|
||||
this.file = file;
|
||||
this.open = true;
|
||||
// This needs to be set before download to open the modal
|
||||
this.filename = file.name;
|
||||
this.file = file;
|
||||
this.open = true;
|
||||
|
||||
if (previewConfig) {
|
||||
this.type = previewConfig.type;
|
||||
if (previewConfig) {
|
||||
this.type = previewConfig.type;
|
||||
|
||||
const response = await apiHandler.downloadFile(this.$route.params.bucket, file.key, previewConfig, (progressEvent) => {
|
||||
this.downloadProgress = progressEvent.loaded / progressEvent.total;
|
||||
}, this.abortControl);
|
||||
const response = await apiHandler.downloadFile(
|
||||
this.$route.params.bucket,
|
||||
file.key,
|
||||
previewConfig,
|
||||
(progressEvent) => {
|
||||
this.downloadProgress = progressEvent.loaded / progressEvent.total;
|
||||
},
|
||||
this.abortControl,
|
||||
);
|
||||
|
||||
let data;
|
||||
if (previewConfig.downloadType === "objectUrl") {
|
||||
const blob = new Blob([response.data]);
|
||||
data = URL.createObjectURL(blob);
|
||||
} else if (previewConfig.downloadType === "blob") {
|
||||
data = response.data;
|
||||
} else {
|
||||
data = response.data;
|
||||
}
|
||||
let data;
|
||||
if (previewConfig.downloadType === "objectUrl") {
|
||||
const blob = new Blob([response.data]);
|
||||
data = URL.createObjectURL(blob);
|
||||
} else if (previewConfig.downloadType === "blob") {
|
||||
data = response.data;
|
||||
} else {
|
||||
data = response.data;
|
||||
}
|
||||
|
||||
this.fileData = data;
|
||||
}
|
||||
this.fileData = data;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (this.abortControl) {
|
||||
this.abortControl.abort();
|
||||
}
|
||||
|
||||
},
|
||||
close() {
|
||||
if (this.abortControl) {
|
||||
this.abortControl.abort();
|
||||
}
|
||||
this.cancelEdit();
|
||||
|
||||
this.cancelEdit();
|
||||
// console.log('call')
|
||||
if (this.$route.params.file) {
|
||||
if (this.$route.params.folder === ROOT_FOLDER) {
|
||||
// File was in root folder, mount files home
|
||||
this.$router.push({
|
||||
name: "files-home",
|
||||
params: {
|
||||
bucket: this.$route.params.bucket,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// File was in folder, mount that folder
|
||||
this.$router.push({
|
||||
name: "files-folder",
|
||||
params: {
|
||||
bucket: this.$route.params.bucket,
|
||||
folder: this.$route.params.folder,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('call')
|
||||
if (this.$route.params.file) {
|
||||
if (this.$route.params.folder === ROOT_FOLDER) {
|
||||
this.type = undefined;
|
||||
this.fileData = undefined;
|
||||
this.filename = undefined;
|
||||
this.abortControl = undefined;
|
||||
this.downloadProgress = 0;
|
||||
},
|
||||
markdownParser(text) {
|
||||
return parseMarkdown(text);
|
||||
},
|
||||
csvParser: (text) => {
|
||||
let result = "";
|
||||
const rows = text.split("\n");
|
||||
if (rows.length === 0) {
|
||||
return "<h2>Empty csv</h2>";
|
||||
}
|
||||
|
||||
// File was in root folder, mount files home
|
||||
this.$router.push({
|
||||
name: "files-home",
|
||||
params: {
|
||||
bucket: this.$route.params.bucket
|
||||
}
|
||||
});
|
||||
for (const [index, row] of rows.entries()) {
|
||||
let line = "";
|
||||
const columns = row
|
||||
.split(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g)
|
||||
.filter((item) => {
|
||||
return item !== "" && item !== ",";
|
||||
});
|
||||
|
||||
} else {
|
||||
for (const col of columns) {
|
||||
if (index === 0) {
|
||||
line += `<th>${col.replaceAll('"', "")}</th>`;
|
||||
} else {
|
||||
line += `<td>${col.replaceAll('"', "")}</td>`;
|
||||
}
|
||||
}
|
||||
|
||||
// File was in folder, mount that folder
|
||||
this.$router.push({
|
||||
name: "files-folder",
|
||||
params: {
|
||||
bucket: this.$route.params.bucket,
|
||||
folder: this.$route.params.folder
|
||||
}
|
||||
});
|
||||
result += `<tr>${line}</tr>`;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return `<table class="table">${result}</table>`;
|
||||
},
|
||||
|
||||
this.type = undefined;
|
||||
this.fileData = undefined;
|
||||
this.filename = undefined;
|
||||
this.abortControl = undefined;
|
||||
this.downloadProgress = 0;
|
||||
},
|
||||
markdownParser(text) {
|
||||
return parseMarkdown(text);
|
||||
},
|
||||
csvParser: function(text) {
|
||||
let result = "";
|
||||
const rows = text.split("\n");
|
||||
if (rows.length === 0) {
|
||||
return "<h2>Empty csv</h2>";
|
||||
}
|
||||
// Edit functions
|
||||
enableEdit: function () {
|
||||
if (typeof this.fileData === "object") {
|
||||
this.fileDataEdited = JSON.stringify(this.fileData, null, 2);
|
||||
} else {
|
||||
this.fileDataEdited = this.fileData;
|
||||
}
|
||||
this.editMode = true;
|
||||
},
|
||||
cancelEdit: function () {
|
||||
this.editMode = false;
|
||||
this.fileDataEdited = undefined;
|
||||
},
|
||||
validateEdit: (type, content) => {
|
||||
if (type === "json") {
|
||||
try {
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [index, row] of rows.entries()) {
|
||||
let line = "";
|
||||
const columns = row.split(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g).filter(item => {
|
||||
return item !== "" && item !== ",";
|
||||
});
|
||||
return true;
|
||||
},
|
||||
saveEdit: async function () {
|
||||
const isValid = this.validateEdit(this.type, this.fileDataEdited);
|
||||
if (!isValid) {
|
||||
this.q.notify({
|
||||
type: "negative",
|
||||
message: `Content is not valid ${this.type}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const col of columns) {
|
||||
if (index === 0) {
|
||||
line += `<th>${col.replaceAll("\"", "")}</th>`;
|
||||
} else {
|
||||
line += `<td>${col.replaceAll("\"", "")}</td>`;
|
||||
}
|
||||
}
|
||||
const notif = this.q.notify({
|
||||
group: false,
|
||||
spinner: true,
|
||||
message: "Updating file...",
|
||||
caption: "0%",
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
result += `<tr>${line}</tr>`;
|
||||
}
|
||||
const blobProperties = {};
|
||||
if (this.file.httpMetadata?.contentType) {
|
||||
blobProperties["type"] = this.file.httpMetadata?.contentType;
|
||||
}
|
||||
const newFile = new Blob([this.fileDataEdited], blobProperties);
|
||||
|
||||
return `<table class="table">${result}</table>`;
|
||||
},
|
||||
await apiHandler.uploadObjects(
|
||||
newFile,
|
||||
this.file.key,
|
||||
this.selectedBucket,
|
||||
(progressEvent) => {
|
||||
notif({
|
||||
caption: `${Number.parseInt((progressEvent.loaded * 100) / newFile.size)}%`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Edit functions
|
||||
enableEdit: function() {
|
||||
if (typeof this.fileData === 'object') {
|
||||
this.fileDataEdited = JSON.stringify(this.fileData, null, 2)
|
||||
} else {
|
||||
this.fileDataEdited = this.fileData;
|
||||
}
|
||||
this.editMode = true;
|
||||
},
|
||||
cancelEdit: function() {
|
||||
this.editMode = false;
|
||||
this.fileDataEdited = undefined;
|
||||
},
|
||||
validateEdit: function(type, content) {
|
||||
if (type === 'json') {
|
||||
try {
|
||||
JSON.parse(content)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
notif({
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
caption: "100%",
|
||||
message: "File updated!",
|
||||
timeout: 5000, // we will timeout it in 5s
|
||||
});
|
||||
|
||||
return true
|
||||
},
|
||||
saveEdit: async function() {
|
||||
const isValid = this.validateEdit(this.type, this.fileDataEdited)
|
||||
if (!isValid) {
|
||||
this.q.notify({
|
||||
type: 'negative',
|
||||
message: `Content is not valid ${this.type}.`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const notif = this.q.notify({
|
||||
group: false,
|
||||
spinner: true,
|
||||
message: `Updating file...`,
|
||||
caption: '0%',
|
||||
timeout: 0
|
||||
})
|
||||
|
||||
const blobProperties = {}
|
||||
if (this.file.httpMetadata?.contentType) {
|
||||
blobProperties['type'] = this.file.httpMetadata?.contentType
|
||||
}
|
||||
const newFile = new Blob([this.fileDataEdited], blobProperties);
|
||||
|
||||
await apiHandler.uploadObjects(newFile, this.file.key, this.selectedBucket, (progressEvent) => {
|
||||
notif({
|
||||
caption: `${parseInt((progressEvent.loaded) * 100 / (newFile.size))}%`
|
||||
});
|
||||
});
|
||||
|
||||
notif({
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
caption: '100%',
|
||||
message: 'File updated!',
|
||||
timeout: 5000 // we will timeout it in 5s
|
||||
})
|
||||
|
||||
this.cancelEdit()
|
||||
this.openFile(this.file)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
q: useQuasar()
|
||||
};
|
||||
}
|
||||
this.cancelEdit();
|
||||
this.openFile(this.file);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
q: useQuasar(),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,36 +17,34 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pdf from 'components/preview/PdfVuer.vue'
|
||||
import pdf from "components/preview/PdfVuer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
pdf
|
||||
},
|
||||
props: ['pdfUrl'],
|
||||
data () {
|
||||
return {
|
||||
page: 1,
|
||||
numPages: 0,
|
||||
pdfdata: undefined,
|
||||
errors: [],
|
||||
scale: 'page-width'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getPdf()
|
||||
},
|
||||
methods: {
|
||||
getPdf () {
|
||||
const self = this
|
||||
|
||||
self.pdfdata = pdf.createLoadingTask(this.pdfUrl)
|
||||
self.pdfdata.then((pdf) => {
|
||||
self.numPages = pdf.numPages
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
components: {
|
||||
pdf,
|
||||
},
|
||||
props: ["pdfUrl"],
|
||||
data() {
|
||||
return {
|
||||
page: 1,
|
||||
numPages: 0,
|
||||
pdfdata: undefined,
|
||||
errors: [],
|
||||
scale: "page-width",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.getPdf();
|
||||
},
|
||||
methods: {
|
||||
getPdf() {
|
||||
this.pdfdata = pdf.createLoadingTask(this.pdfUrl);
|
||||
this.pdfdata.then((pdf) => {
|
||||
this.numPages = pdf.numPages;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
/* Page content */
|
||||
|
||||
@@ -8,235 +8,237 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
'use strict';
|
||||
import { getDocument } from 'pdfjs-dist';
|
||||
import { getDocument } from "pdfjs-dist";
|
||||
import PdfjsWorker from "pdfjs-dist/build/pdf.worker.entry";
|
||||
import {
|
||||
DefaultAnnotationLayerFactory,
|
||||
DefaultTextLayerFactory,
|
||||
PDFFindController,
|
||||
PDFLinkService,
|
||||
PDFPageView,
|
||||
EventBus
|
||||
} from 'pdfjs-dist/web/pdf_viewer.js';
|
||||
import resizeSensor from 'pdfvuer/src/resize.vue'
|
||||
import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry'
|
||||
DefaultAnnotationLayerFactory,
|
||||
DefaultTextLayerFactory,
|
||||
EventBus,
|
||||
PDFFindController,
|
||||
PDFLinkService,
|
||||
PDFPageView,
|
||||
} from "pdfjs-dist/web/pdf_viewer.js";
|
||||
import resizeSensor from "pdfvuer/src/resize.vue";
|
||||
function isPDFDocumentLoadingTask(obj) {
|
||||
return typeof (obj) === 'object' && obj !== null && obj.__PDFDocumentLoadingTask === true;
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
obj.__PDFDocumentLoadingTask === true
|
||||
);
|
||||
}
|
||||
|
||||
export function createLoadingTask(src, options) {
|
||||
var source;
|
||||
if (typeof (src) === 'string')
|
||||
source = {
|
||||
url: src
|
||||
};
|
||||
else if (typeof (src) === 'object' && src !== null)
|
||||
source = Object.assign({}, src);
|
||||
else
|
||||
throw new TypeError('invalid src type');
|
||||
let source;
|
||||
if (typeof src === "string")
|
||||
source = {
|
||||
url: src,
|
||||
};
|
||||
else if (typeof src === "object" && src !== null)
|
||||
source = Object.assign({}, src);
|
||||
else throw new TypeError("invalid src type");
|
||||
|
||||
var loadingTask = getDocument(source).promise;
|
||||
loadingTask.__PDFDocumentLoadingTask = true; // since PDFDocumentLoadingTask is not public
|
||||
const loadingTask = getDocument(source).promise;
|
||||
loadingTask.__PDFDocumentLoadingTask = true; // since PDFDocumentLoadingTask is not public
|
||||
|
||||
if (options && options.onPassword)
|
||||
loadingTask.onPassword = options.onPassword;
|
||||
if (options?.onPassword) loadingTask.onPassword = options.onPassword;
|
||||
|
||||
if (options && options.onProgress)
|
||||
loadingTask.onProgress = options.onProgress;
|
||||
if (options?.onProgress) loadingTask.onProgress = options.onProgress;
|
||||
|
||||
return loadingTask;
|
||||
return loadingTask;
|
||||
}
|
||||
|
||||
export default {
|
||||
createLoadingTask: createLoadingTask,
|
||||
components: {
|
||||
resizeSensor
|
||||
},
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Object, Promise],
|
||||
default: '',
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
rotate: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
scale: {
|
||||
type: [Number, String],
|
||||
default: 'page-width',
|
||||
},
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
annotation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
internalSrc: this.src,
|
||||
pdf: null,
|
||||
pdfViewer: null,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pdf: function (val) {
|
||||
var pdfInfo = val.pdfInfo || val._pdfInfo
|
||||
this.$emit('numpages', pdfInfo.numPages);
|
||||
},
|
||||
page: function (val) {
|
||||
var self = this;
|
||||
this.pdf.getPage(val).then(function (pdfPage) {
|
||||
self.pdfViewer.setPdfPage(pdfPage);
|
||||
self.pdfViewer.draw();
|
||||
});
|
||||
},
|
||||
scale: function (val) {
|
||||
this.drawScaled(val);
|
||||
},
|
||||
rotate: function (newRotate) {
|
||||
if (this.pdfViewer) {
|
||||
this.pdfViewer.update(this.scale, newRotate);
|
||||
this.pdfViewer.draw();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
var self = this;
|
||||
if (!isPDFDocumentLoadingTask(self.internalSrc)) {
|
||||
self.internalSrc = createLoadingTask(self.internalSrc);
|
||||
self.$emit('loading', true);
|
||||
}
|
||||
createLoadingTask: createLoadingTask,
|
||||
components: {
|
||||
resizeSensor,
|
||||
},
|
||||
props: {
|
||||
src: {
|
||||
type: [String, Object, Promise],
|
||||
default: "",
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
rotate: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
scale: {
|
||||
type: [Number, String],
|
||||
default: "page-width",
|
||||
},
|
||||
resize: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
annotation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
internalSrc: this.src,
|
||||
pdf: null,
|
||||
pdfViewer: null,
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
pdf: function (val) {
|
||||
const pdfInfo = val.pdfInfo || val._pdfInfo;
|
||||
this.$emit("numpages", pdfInfo.numPages);
|
||||
},
|
||||
page: function (val) {
|
||||
this.pdf.getPage(val).then((pdfPage) => {
|
||||
this.pdfViewer.setPdfPage(pdfPage);
|
||||
this.pdfViewer.draw();
|
||||
});
|
||||
},
|
||||
scale: function (val) {
|
||||
this.drawScaled(val);
|
||||
},
|
||||
rotate: function (newRotate) {
|
||||
if (this.pdfViewer) {
|
||||
this.pdfViewer.update(this.scale, newRotate);
|
||||
this.pdfViewer.draw();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
if (!isPDFDocumentLoadingTask(this.internalSrc)) {
|
||||
this.internalSrc = createLoadingTask(this.internalSrc);
|
||||
this.$emit("loading", true);
|
||||
}
|
||||
|
||||
var container = this.$refs.container;
|
||||
var eventBus = new EventBus();
|
||||
const container = this.$refs.container;
|
||||
const eventBus = new EventBus();
|
||||
|
||||
// (Optionally) enable hyperlinks within PDF files.
|
||||
self.pdfLinkService = new PDFLinkService({
|
||||
eventBus: eventBus,
|
||||
externalLinkTarget: 2
|
||||
});
|
||||
// (Optionally) enable hyperlinks within PDF files.
|
||||
this.pdfLinkService = new PDFLinkService({
|
||||
eventBus: eventBus,
|
||||
externalLinkTarget: 2,
|
||||
});
|
||||
|
||||
// (Optionally) enable find controller.
|
||||
self.pdfFindController = new PDFFindController({
|
||||
eventBus: eventBus,
|
||||
linkService: self.pdfLinkService
|
||||
});
|
||||
// (Optionally) enable find controller.
|
||||
this.pdfFindController = new PDFFindController({
|
||||
eventBus: eventBus,
|
||||
linkService: this.pdfLinkService,
|
||||
});
|
||||
|
||||
let annotationLayer = undefined,
|
||||
textLayer = undefined;
|
||||
let annotationLayer = undefined;
|
||||
let textLayer = undefined;
|
||||
|
||||
if (self.annotation) {
|
||||
annotationLayer = new DefaultAnnotationLayerFactory();
|
||||
}
|
||||
if (self.text) {
|
||||
textLayer = new DefaultTextLayerFactory();
|
||||
}
|
||||
if (this.annotation) {
|
||||
annotationLayer = new DefaultAnnotationLayerFactory();
|
||||
}
|
||||
if (this.text) {
|
||||
textLayer = new DefaultTextLayerFactory();
|
||||
}
|
||||
|
||||
self.internalSrc
|
||||
.then(function (pdfDocument) {
|
||||
// Document loaded, retrieving the page.
|
||||
self.pdf = pdfDocument;
|
||||
return pdfDocument.getPage(self.page)
|
||||
}).then(function (pdfPage) {
|
||||
// Creating the page view with default parameters.
|
||||
self.pdfViewer = new PDFPageView({
|
||||
container: container,
|
||||
id: self.page,
|
||||
scale: 1,
|
||||
defaultViewport: pdfPage.getViewport({
|
||||
scale: 1
|
||||
}),
|
||||
eventBus: eventBus,
|
||||
textLayerFactory: textLayer,
|
||||
annotationLayerFactory: annotationLayer,
|
||||
});
|
||||
// Associates the actual page with the view, and drawing it
|
||||
self.pdfViewer.setPdfPage(pdfPage);
|
||||
// Set up a scrollPageIntoView function for our links
|
||||
var viewer = {
|
||||
scrollPageIntoView: function(params) {
|
||||
// Send an event when clicking internal links so we can handle loading/scrolling to the correct page
|
||||
self.$emit('link-clicked', params);
|
||||
},
|
||||
};
|
||||
self.pdfLinkService.setDocument(self.pdf);
|
||||
self.pdfLinkService.setViewer(viewer);
|
||||
self.pdfFindController.setDocument(self.pdf);
|
||||
self.drawScaled(self.scale);
|
||||
this.internalSrc
|
||||
.then((pdfDocument) => {
|
||||
// Document loaded, retrieving the page.
|
||||
this.pdf = pdfDocument;
|
||||
return pdfDocument.getPage(this.page);
|
||||
})
|
||||
.then((pdfPage) => {
|
||||
// Creating the page view with default parameters.
|
||||
this.pdfViewer = new PDFPageView({
|
||||
container: container,
|
||||
id: this.page,
|
||||
scale: 1,
|
||||
defaultViewport: pdfPage.getViewport({
|
||||
scale: 1,
|
||||
}),
|
||||
eventBus: eventBus,
|
||||
textLayerFactory: textLayer,
|
||||
annotationLayerFactory: annotationLayer,
|
||||
});
|
||||
// Associates the actual page with the view, and drawing it
|
||||
this.pdfViewer.setPdfPage(pdfPage);
|
||||
// Set up a scrollPageIntoView function for our links
|
||||
const viewer = {
|
||||
scrollPageIntoView: (params) => {
|
||||
// Send an event when clicking internal links so we can handle loading/scrolling to the correct page
|
||||
this.$emit("link-clicked", params);
|
||||
},
|
||||
};
|
||||
this.pdfLinkService.setDocument(this.pdf);
|
||||
this.pdfLinkService.setViewer(viewer);
|
||||
this.pdfFindController.setDocument(this.pdf);
|
||||
this.drawScaled(this.scale);
|
||||
|
||||
self.loading = false;
|
||||
self.$emit('loading', false);
|
||||
}).catch(err => {
|
||||
self.$emit('error', err)
|
||||
self.loading = false
|
||||
self.$emit('loading', false);
|
||||
})
|
||||
},
|
||||
beforeUnmount() {
|
||||
var self = this;
|
||||
if (self.pdfViewer) {
|
||||
self.pdfViewer.destroy();
|
||||
self.pdfViewer = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateScale: function (width = -1, height = -1) {
|
||||
this.pdfViewer.update(1, this.rotate); // Reset scaling to 1 so that "this.pdfViewer.viewport.width" gives proper width;
|
||||
if (width === -1 && height === -1) {
|
||||
width = this.$refs.container.offsetWidth;
|
||||
}
|
||||
this.loading = false;
|
||||
this.$emit("loading", false);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$emit("error", err);
|
||||
this.loading = false;
|
||||
this.$emit("loading", false);
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.pdfViewer) {
|
||||
this.pdfViewer.destroy();
|
||||
this.pdfViewer = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateScale: function (width = -1, height = -1) {
|
||||
this.pdfViewer.update(1, this.rotate); // Reset scaling to 1 so that "this.pdfViewer.viewport.width" gives proper width;
|
||||
if (width === -1 && height === -1) {
|
||||
width = this.$refs.container.offsetWidth;
|
||||
}
|
||||
|
||||
return width / this.pdfViewer.viewport.width;
|
||||
},
|
||||
calculateScaleHeight: function () {
|
||||
this.pdfViewer.update(1, this.rotate); // Reset scaling to 1 so that "this.pdfViewer.viewport.width" gives proper width;
|
||||
var height = this.$refs.container.offsetHeight;
|
||||
var parentel = this.$refs.container.parentElement.parentElement;
|
||||
return parentel.offsetHeight / height;
|
||||
},
|
||||
drawScaled: function (newScale) {
|
||||
if (this.pdfViewer) {
|
||||
if (newScale === 'page-width') {
|
||||
newScale = this.calculateScale();
|
||||
this.$emit("update:scale", newScale);
|
||||
}
|
||||
else if (newScale === 'page-height') {
|
||||
newScale = this.calculateScaleHeight();
|
||||
this.$emit("update:scale", newScale);
|
||||
}
|
||||
this.pdfViewer.update(newScale, this.rotate);
|
||||
// The SimpleLinkService from the DefaultAnnotationLayerFactory doesn't do anything with links so override with our LinkService after it is created
|
||||
if(this.annotation) {
|
||||
this.pdfViewer.annotationLayer = this.pdfViewer.annotationLayerFactory.createAnnotationLayerBuilder(this.pdfViewer.div, this.pdfViewer.pdfPage);
|
||||
this.pdfViewer.annotationLayer.linkService = this.pdfLinkService;
|
||||
}
|
||||
this.pdfViewer.draw();
|
||||
// The findController needs the text layer to have been created in the Draw() function, so link it in now
|
||||
if (this.text) {
|
||||
this.pdfViewer.textLayer.findController = this.pdfFindController;
|
||||
}
|
||||
this.loading = false;
|
||||
this.$emit('loading', false);
|
||||
}
|
||||
},
|
||||
resizeScale: function () {
|
||||
if (this.resize) {
|
||||
this.drawScaled('page-width');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return width / this.pdfViewer.viewport.width;
|
||||
},
|
||||
calculateScaleHeight: function () {
|
||||
this.pdfViewer.update(1, this.rotate); // Reset scaling to 1 so that "this.pdfViewer.viewport.width" gives proper width;
|
||||
const height = this.$refs.container.offsetHeight;
|
||||
const parentel = this.$refs.container.parentElement.parentElement;
|
||||
return parentel.offsetHeight / height;
|
||||
},
|
||||
drawScaled: function (newScale) {
|
||||
if (this.pdfViewer) {
|
||||
if (newScale === "page-width") {
|
||||
newScale = this.calculateScale();
|
||||
this.$emit("update:scale", newScale);
|
||||
} else if (newScale === "page-height") {
|
||||
newScale = this.calculateScaleHeight();
|
||||
this.$emit("update:scale", newScale);
|
||||
}
|
||||
this.pdfViewer.update(newScale, this.rotate);
|
||||
// The SimpleLinkService from the DefaultAnnotationLayerFactory doesn't do anything with links so override with our LinkService after it is created
|
||||
if (this.annotation) {
|
||||
this.pdfViewer.annotationLayer =
|
||||
this.pdfViewer.annotationLayerFactory.createAnnotationLayerBuilder(
|
||||
this.pdfViewer.div,
|
||||
this.pdfViewer.pdfPage,
|
||||
);
|
||||
this.pdfViewer.annotationLayer.linkService = this.pdfLinkService;
|
||||
}
|
||||
this.pdfViewer.draw();
|
||||
// The findController needs the text layer to have been created in the Draw() function, so link it in now
|
||||
if (this.text) {
|
||||
this.pdfViewer.textLayer.findController = this.pdfFindController;
|
||||
}
|
||||
this.loading = false;
|
||||
this.$emit("loading", false);
|
||||
}
|
||||
},
|
||||
resizeScale: function () {
|
||||
if (this.resize) {
|
||||
this.drawScaled("page-width");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style src="pdfjs-dist/web/pdf_viewer.css"></style>
|
||||
|
||||
@@ -8,25 +8,23 @@
|
||||
import * as fflate from "fflate";
|
||||
|
||||
export default {
|
||||
props: ['filedata'],
|
||||
data: function () {
|
||||
return {
|
||||
data: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const parsedData = new Uint8Array(this.filedata);
|
||||
const decompressed = fflate.decompressSync(parsedData);
|
||||
props: ["filedata"],
|
||||
data: () => ({
|
||||
data: null,
|
||||
}),
|
||||
mounted() {
|
||||
const parsedData = new Uint8Array(this.filedata);
|
||||
const decompressed = fflate.decompressSync(parsedData);
|
||||
|
||||
const origText = fflate.strFromU8(decompressed);
|
||||
const origText = fflate.strFromU8(decompressed);
|
||||
|
||||
this.data = origText.split('\n').map(val => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(val), null, 2)
|
||||
} catch (e) {
|
||||
return val
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this.data = origText.split("\n").map((val) => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(val), null, 2);
|
||||
} catch (e) {
|
||||
return val;
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -23,296 +23,320 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { apiHandler, decode, ROOT_FOLDER } from "src/appUtils";
|
||||
import { useQuasar } from "quasar";
|
||||
import { ROOT_FOLDER, apiHandler, decode } from "src/appUtils";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
isHover: false,
|
||||
filelist: [], // Store our uploaded files
|
||||
dragContainer: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (this.$route.params.folder && this.$route.params.folder !== ROOT_FOLDER) {
|
||||
return decode(this.$route.params.folder)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFilesUploader () {
|
||||
this.$refs.filesUploader.click()
|
||||
},
|
||||
openFoldersUploader () {
|
||||
this.$refs.foldersUploader.click()
|
||||
},
|
||||
dragover (event) {
|
||||
if (this.mainStore.apiReadonly || this.isHover === true) {
|
||||
return
|
||||
}
|
||||
data: () => ({
|
||||
isHover: false,
|
||||
filelist: [], // Store our uploaded files
|
||||
dragContainer: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
}),
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (
|
||||
this.$route.params.folder &&
|
||||
this.$route.params.folder !== ROOT_FOLDER
|
||||
) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFilesUploader() {
|
||||
this.$refs.filesUploader.click();
|
||||
},
|
||||
openFoldersUploader() {
|
||||
this.$refs.foldersUploader.click();
|
||||
},
|
||||
dragover(event) {
|
||||
if (this.mainStore.apiReadonly || this.isHover === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coordinates = this.$refs.dragContainer.getBoundingClientRect()
|
||||
this.dragContainer = {
|
||||
top: coordinates.top,
|
||||
bottom: coordinates.bottom,
|
||||
left: coordinates.left,
|
||||
right: coordinates.right
|
||||
}
|
||||
const coordinates = this.$refs.dragContainer.getBoundingClientRect();
|
||||
this.dragContainer = {
|
||||
top: coordinates.top,
|
||||
bottom: coordinates.bottom,
|
||||
left: coordinates.left,
|
||||
right: coordinates.right,
|
||||
};
|
||||
|
||||
this.isHover = true
|
||||
},
|
||||
dragleave (event) {
|
||||
if (this.isHover === false) {
|
||||
return
|
||||
}
|
||||
this.isHover = true;
|
||||
},
|
||||
dragleave(event) {
|
||||
if (this.isHover === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event.clientX < this.dragContainer.left ||
|
||||
event.clientX > this.dragContainer.right ||
|
||||
event.clientY > this.dragContainer.bottom ||
|
||||
event.clientY < this.dragContainer.top
|
||||
) {
|
||||
this.isHover = false
|
||||
}
|
||||
},
|
||||
inputFiles (event) {
|
||||
this.uploadFiles({
|
||||
'': event.target.files
|
||||
})
|
||||
},
|
||||
inputFolders (event) {
|
||||
const folders = {}
|
||||
for (const file of event.target.files) {
|
||||
const lastIndex = file.webkitRelativePath.lastIndexOf('/')
|
||||
const path = file.webkitRelativePath.slice(0, lastIndex)
|
||||
if (
|
||||
event.clientX < this.dragContainer.left ||
|
||||
event.clientX > this.dragContainer.right ||
|
||||
event.clientY > this.dragContainer.bottom ||
|
||||
event.clientY < this.dragContainer.top
|
||||
) {
|
||||
this.isHover = false;
|
||||
}
|
||||
},
|
||||
inputFiles(event) {
|
||||
this.uploadFiles({
|
||||
"": event.target.files,
|
||||
});
|
||||
},
|
||||
inputFolders(event) {
|
||||
const folders = {};
|
||||
for (const file of event.target.files) {
|
||||
const lastIndex = file.webkitRelativePath.lastIndexOf("/");
|
||||
const path = file.webkitRelativePath.slice(0, lastIndex);
|
||||
|
||||
if (folders[path] === undefined) {
|
||||
folders[path] = []
|
||||
}
|
||||
if (folders[path] === undefined) {
|
||||
folders[path] = [];
|
||||
}
|
||||
|
||||
folders[path].push(file)
|
||||
}
|
||||
folders[path].push(file);
|
||||
}
|
||||
|
||||
this.uploadFiles(folders)
|
||||
},
|
||||
async uploadFiles (folders) {
|
||||
let totalFiles = 0
|
||||
let totalSize = 0
|
||||
const filenames = []
|
||||
this.uploadFiles(folders);
|
||||
},
|
||||
async uploadFiles(folders) {
|
||||
let totalFiles = 0;
|
||||
let totalSize = 0;
|
||||
const filenames = [];
|
||||
|
||||
// Create folders and count files
|
||||
for (const [folder, files] of Object.entries(folders)) {
|
||||
// if (folder.slice(-1) === '/') {
|
||||
// folder = folder.slice(0, -1)
|
||||
// }
|
||||
// Create folders and count files
|
||||
for (const [folder, files] of Object.entries(folders)) {
|
||||
// if (folder.slice(-1) === '/') {
|
||||
// folder = folder.slice(0, -1)
|
||||
// }
|
||||
|
||||
if (folder !== '') {
|
||||
let folderKey = this.selectedFolder + folder + '/'
|
||||
if (folder !== "") {
|
||||
const folderKey = `${this.selectedFolder + folder}/`;
|
||||
|
||||
await apiHandler.createFolder(folderKey, this.selectedBucket)
|
||||
}
|
||||
await apiHandler.createFolder(folderKey, this.selectedBucket);
|
||||
}
|
||||
|
||||
totalFiles += files.length
|
||||
totalFiles += files.length;
|
||||
|
||||
for (const file of files) {
|
||||
filenames.push(file.name)
|
||||
totalSize += file.size
|
||||
}
|
||||
}
|
||||
for (const file of files) {
|
||||
filenames.push(file.name);
|
||||
totalSize += file.size;
|
||||
}
|
||||
}
|
||||
|
||||
this.$bus.emit('fetchFiles')
|
||||
// self.$store.dispatch('addUploadingFiles', filenames) // TODO!
|
||||
this.$bus.emit("fetchFiles");
|
||||
// self.$store.dispatch('addUploadingFiles', filenames) // TODO!
|
||||
|
||||
const notif = this.q.notify({
|
||||
group: false,
|
||||
spinner: true,
|
||||
message: `Uploading files 1/${filenames.length}...`,
|
||||
caption: '0%',
|
||||
timeout: 0
|
||||
})
|
||||
const notif = this.q.notify({
|
||||
group: false,
|
||||
spinner: true,
|
||||
message: `Uploading files 1/${filenames.length}...`,
|
||||
caption: "0%",
|
||||
timeout: 0,
|
||||
});
|
||||
|
||||
// Upload files
|
||||
let uploadCount = 0
|
||||
let uploadSize = 0
|
||||
for (const [folder, files] of Object.entries(folders)) {
|
||||
notif({
|
||||
message: `Uploading files ${uploadCount+1}/${filenames.length}...`,
|
||||
caption: `${parseInt((uploadCount+1)*100/(filenames.length))}%` // +1 because still needs to delete the folder
|
||||
})
|
||||
// Upload files
|
||||
let uploadCount = 0;
|
||||
let uploadSize = 0;
|
||||
for (const [folder, files] of Object.entries(folders)) {
|
||||
notif({
|
||||
message: `Uploading files ${uploadCount + 1}/${filenames.length}...`,
|
||||
caption: `${Number.parseInt(((uploadCount + 1) * 100) / filenames.length)}%`, // +1 because still needs to delete the folder
|
||||
});
|
||||
|
||||
let targetFolder = this.selectedFolder + folder
|
||||
if (targetFolder.length > 0 && targetFolder.slice(-1) !== '/') {
|
||||
targetFolder += '/'
|
||||
}
|
||||
let targetFolder = this.selectedFolder + folder;
|
||||
if (targetFolder.length > 0 && targetFolder.slice(-1) !== "/") {
|
||||
targetFolder += "/";
|
||||
}
|
||||
|
||||
if(targetFolder === '/' || targetFolder === ROOT_FOLDER) {
|
||||
targetFolder = ''
|
||||
}
|
||||
if (targetFolder === "/" || targetFolder === ROOT_FOLDER) {
|
||||
targetFolder = "";
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
uploadCount += 1
|
||||
const key = targetFolder + file.name
|
||||
for (const file of files) {
|
||||
uploadCount += 1;
|
||||
const key = targetFolder + file.name;
|
||||
|
||||
const chunkSize = 95 * 1024 * 1024
|
||||
// Files bigger than 100MB require multipart upload
|
||||
if (file.size > chunkSize) {
|
||||
const { uploadId } = (await apiHandler.multipartCreate(file, key, this.selectedBucket)).data
|
||||
const chunkSize = 95 * 1024 * 1024;
|
||||
// Files bigger than 100MB require multipart upload
|
||||
if (file.size > chunkSize) {
|
||||
const { uploadId } = (
|
||||
await apiHandler.multipartCreate(file, key, this.selectedBucket)
|
||||
).data;
|
||||
|
||||
let partNumber = 1
|
||||
const parts = []
|
||||
// console.log('total: ', file.size)
|
||||
// console.log('chunk: ', chunkSize)
|
||||
let partNumber = 1;
|
||||
const parts = [];
|
||||
// console.log('total: ', file.size)
|
||||
// console.log('chunk: ', chunkSize)
|
||||
|
||||
for (let start = 0; start < file.size; start += chunkSize) {
|
||||
const end = Math.min(start + chunkSize, file.size)
|
||||
const chunk = file.slice(start, end)
|
||||
// console.log(`${start} -> ${end}`)
|
||||
for (let start = 0; start < file.size; start += chunkSize) {
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
// console.log(`${start} -> ${end}`)
|
||||
|
||||
const { data } = await apiHandler.multipartUpload(uploadId, partNumber, this.selectedBucket, key, chunk, (progressEvent) => {
|
||||
//console.log((start + progressEvent.loaded) * 100 / file.size)
|
||||
notif({
|
||||
caption: `${parseInt((uploadSize+start + progressEvent.loaded)*100/(totalSize))}%`
|
||||
})
|
||||
// self.$store.dispatch('setUploadProgress', {
|
||||
// filename: file.name,
|
||||
// progress: (start + progressEvent.loaded) * 100 / file.size
|
||||
// }) // TODO
|
||||
})
|
||||
const { data } = await apiHandler.multipartUpload(
|
||||
uploadId,
|
||||
partNumber,
|
||||
this.selectedBucket,
|
||||
key,
|
||||
chunk,
|
||||
(progressEvent) => {
|
||||
//console.log((start + progressEvent.loaded) * 100 / file.size)
|
||||
notif({
|
||||
caption: `${Number.parseInt(((uploadSize + start + progressEvent.loaded) * 100) / totalSize)}%`,
|
||||
});
|
||||
// self.$store.dispatch('setUploadProgress', {
|
||||
// filename: file.name,
|
||||
// progress: (start + progressEvent.loaded) * 100 / file.size
|
||||
// }) // TODO
|
||||
},
|
||||
);
|
||||
|
||||
parts.push(data)
|
||||
partNumber += 1
|
||||
}
|
||||
parts.push(data);
|
||||
partNumber += 1;
|
||||
}
|
||||
|
||||
await apiHandler.multipartComplete(file, key, this.selectedBucket, parts, uploadId)
|
||||
} else {
|
||||
await apiHandler.uploadObjects(file, key, this.selectedBucket, (progressEvent) => {
|
||||
//console.log(progressEvent.loaded * 100 / file.size)
|
||||
notif({
|
||||
caption: `${parseInt((uploadSize+progressEvent.loaded)*100/(totalSize))}%`
|
||||
})
|
||||
// self.$store.dispatch('setUploadProgress', {
|
||||
// filename: file.name,
|
||||
// progress: progressEvent.loaded * 100 / file.size
|
||||
// }) // TODO
|
||||
})
|
||||
}
|
||||
await apiHandler.multipartComplete(
|
||||
file,
|
||||
key,
|
||||
this.selectedBucket,
|
||||
parts,
|
||||
uploadId,
|
||||
);
|
||||
} else {
|
||||
await apiHandler.uploadObjects(
|
||||
file,
|
||||
key,
|
||||
this.selectedBucket,
|
||||
(progressEvent) => {
|
||||
//console.log(progressEvent.loaded * 100 / file.size)
|
||||
notif({
|
||||
caption: `${Number.parseInt(((uploadSize + progressEvent.loaded) * 100) / totalSize)}%`,
|
||||
});
|
||||
// self.$store.dispatch('setUploadProgress', {
|
||||
// filename: file.name,
|
||||
// progress: progressEvent.loaded * 100 / file.size
|
||||
// }) // TODO
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
uploadSize += file.size
|
||||
}
|
||||
}
|
||||
uploadSize += file.size;
|
||||
}
|
||||
}
|
||||
|
||||
notif({
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
caption: '100%',
|
||||
message: 'Files Uploaded!',
|
||||
timeout: 5000 // we will timeout it in 5s
|
||||
})
|
||||
notif({
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
caption: "100%",
|
||||
message: "Files Uploaded!",
|
||||
timeout: 5000, // we will timeout it in 5s
|
||||
});
|
||||
|
||||
this.$bus.emit('fetchFiles')
|
||||
},
|
||||
async traverseFileTree (item, path, folders, depth) {
|
||||
const self = this
|
||||
path = path || ''
|
||||
this.$bus.emit("fetchFiles");
|
||||
},
|
||||
async traverseFileTree(item, path, folders, depth) {
|
||||
path = path || "";
|
||||
|
||||
// Root files are handled outside
|
||||
if (item.isFile && depth > 0) {
|
||||
const filePromise = new Promise(function (resolve, reject) {
|
||||
item.file(function (file) {
|
||||
folders[path].push(file)
|
||||
// Root files are handled outside
|
||||
if (item.isFile && depth > 0) {
|
||||
const filePromise = new Promise((resolve, reject) => {
|
||||
item.file((file) => {
|
||||
folders[path].push(file);
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await filePromise
|
||||
} else if (item.isDirectory) {
|
||||
const newPath = path ? path + '/' + item.name : item.name
|
||||
if (folders[newPath] === undefined) {
|
||||
folders[newPath] = []
|
||||
}
|
||||
await filePromise;
|
||||
} else if (item.isDirectory) {
|
||||
const newPath = path ? `${path}/${item.name}` : item.name;
|
||||
if (folders[newPath] === undefined) {
|
||||
folders[newPath] = [];
|
||||
}
|
||||
|
||||
// Get folder contents
|
||||
const dirReader = item.createReader()
|
||||
// Get folder contents
|
||||
const dirReader = item.createReader();
|
||||
|
||||
const promise = new Promise(function (resolve, reject) {
|
||||
dirReader.readEntries(async function (entries) {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
await self.traverseFileTree(entries[i], newPath, folders, depth + 1)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(async (entries) => {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
await this.traverseFileTree(
|
||||
entries[i],
|
||||
newPath,
|
||||
folders,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
await promise
|
||||
}
|
||||
},
|
||||
async drop (event) {
|
||||
if (!this.isHover) {
|
||||
return
|
||||
}
|
||||
await promise;
|
||||
}
|
||||
},
|
||||
async drop(event) {
|
||||
if (!this.isHover) {
|
||||
return;
|
||||
}
|
||||
|
||||
// files uploaded on root
|
||||
const rootFiles = event.dataTransfer.files
|
||||
// files uploaded on root
|
||||
const rootFiles = event.dataTransfer.files;
|
||||
|
||||
// folders and files
|
||||
const items = await event.dataTransfer.items
|
||||
const folders = {}
|
||||
// folders and files
|
||||
const items = await event.dataTransfer.items;
|
||||
const folders = {};
|
||||
|
||||
for (const item of items) {
|
||||
// webkitGetAsEntry is where the magic happens
|
||||
const entry = item.webkitGetAsEntry()
|
||||
if (entry) {
|
||||
await this.traverseFileTree(entry, '', folders, 0)
|
||||
}
|
||||
}
|
||||
for (const item of items) {
|
||||
// webkitGetAsEntry is where the magic happens
|
||||
const entry = item.webkitGetAsEntry();
|
||||
if (entry) {
|
||||
await this.traverseFileTree(entry, "", folders, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Root files also include the root folders
|
||||
const cleanedRootFiles = []
|
||||
for (const rootFile of rootFiles) {
|
||||
if (folders[rootFile.name] === undefined) {
|
||||
cleanedRootFiles.push(rootFile)
|
||||
}
|
||||
}
|
||||
// Root files also include the root folders
|
||||
const cleanedRootFiles = [];
|
||||
for (const rootFile of rootFiles) {
|
||||
if (folders[rootFile.name] === undefined) {
|
||||
cleanedRootFiles.push(rootFile);
|
||||
}
|
||||
}
|
||||
|
||||
folders[''] = cleanedRootFiles
|
||||
folders[""] = cleanedRootFiles;
|
||||
|
||||
// console.log(cleanedRootFiles)
|
||||
// console.log(folders)
|
||||
// console.log(cleanedRootFiles)
|
||||
// console.log(folders)
|
||||
|
||||
this.uploadFiles(folders)
|
||||
this.uploadFiles(folders);
|
||||
|
||||
this.isHover = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$bus.on('openFilesUploader', this.openFilesUploader)
|
||||
this.$bus.on('openFoldersUploader', this.openFoldersUploader)
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$bus.off('openFilesUploader')
|
||||
this.$bus.off('openFoldersUploader')
|
||||
},
|
||||
setup () {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar()
|
||||
}
|
||||
},
|
||||
}
|
||||
this.isHover = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$bus.on("openFilesUploader", this.openFilesUploader);
|
||||
this.$bus.on("openFoldersUploader", this.openFoldersUploader);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$bus.off("openFilesUploader");
|
||||
this.$bus.off("openFoldersUploader");
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar(),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
close: function () {
|
||||
this.$store.dispatch('clearUploadingFiles')
|
||||
}
|
||||
}
|
||||
}
|
||||
methods: {
|
||||
close: function () {
|
||||
this.$store.dispatch("clearUploadingFiles");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AuthLayout',
|
||||
name: "AuthLayout",
|
||||
|
||||
components: {},
|
||||
components: {},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -25,41 +25,44 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import LeftSidebar from "components/main/LeftSidebar.vue";
|
||||
import RightSidebar from "components/main/RightSidebar.vue";
|
||||
import TopBar from "components/main/Topbar.vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { ref } from "vue";
|
||||
|
||||
export default {
|
||||
name: 'MainLayout',
|
||||
components: { TopBar, RightSidebar, LeftSidebar },
|
||||
created() {
|
||||
const mainStore = useMainStore()
|
||||
mainStore.loadUserDisks().then((buckets) => {
|
||||
if (this.$route.path === '/') {
|
||||
this.$router.push({ name: `files-home`, params: { bucket: buckets[0].name }})
|
||||
}
|
||||
});
|
||||
},
|
||||
setup () {
|
||||
const leftDrawerOpen = ref(false)
|
||||
const rightDrawerOpen = ref(false)
|
||||
name: "MainLayout",
|
||||
components: { TopBar, RightSidebar, LeftSidebar },
|
||||
created() {
|
||||
const mainStore = useMainStore();
|
||||
mainStore.loadUserDisks().then((buckets) => {
|
||||
if (this.$route.path === "/") {
|
||||
this.$router.push({
|
||||
name: "files-home",
|
||||
params: { bucket: buckets[0].name },
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setup() {
|
||||
const leftDrawerOpen = ref(false);
|
||||
const rightDrawerOpen = ref(false);
|
||||
|
||||
return {
|
||||
leftDrawerOpen,
|
||||
toggleLeftDrawer () {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||
},
|
||||
return {
|
||||
leftDrawerOpen,
|
||||
toggleLeftDrawer() {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||
},
|
||||
|
||||
rightDrawerOpen,
|
||||
updateRightDrawer (state) {
|
||||
rightDrawerOpen.value = state
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateRightDrawer(false)
|
||||
}
|
||||
}
|
||||
rightDrawerOpen,
|
||||
updateRightDrawer(state) {
|
||||
rightDrawerOpen.value = state;
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateRightDrawer(false);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorNotFound'
|
||||
})
|
||||
name: "ErrorNotFound",
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomePage',
|
||||
setup () {
|
||||
return {
|
||||
mainStore: useMainStore()
|
||||
}
|
||||
},
|
||||
})
|
||||
name: "HomePage",
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -45,37 +45,37 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { useAuthStore } from 'stores/auth-store';
|
||||
const authStore = useAuthStore()
|
||||
import { useAuthStore } from "stores/auth-store";
|
||||
import { defineComponent } from "vue";
|
||||
const authStore = useAuthStore();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'login-page',
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showError: '',
|
||||
form: {
|
||||
username: '',
|
||||
password: '',
|
||||
remind: true
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onSubmit() {
|
||||
this.loading = true
|
||||
try {
|
||||
await authStore.LogIn(this.$router, this.form)
|
||||
this.showError = '';
|
||||
} catch (error) {
|
||||
this.showError = error.message;
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
name: "login-page",
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
showError: "",
|
||||
form: {
|
||||
username: "",
|
||||
password: "",
|
||||
remind: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async onSubmit() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await authStore.LogIn(this.$router, this.form);
|
||||
this.showError = "";
|
||||
} catch (error) {
|
||||
this.showError = error.message;
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -91,165 +91,192 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { api } from "boot/axios";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { apiHandler, decode, encode, timeSince } from "../../appUtils";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
import { apiHandler, decode, encode, timeSince } from "../../appUtils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "EmailFolderPage",
|
||||
data: function() {
|
||||
return {
|
||||
srcdoc: null,
|
||||
file: null,
|
||||
fileHead: null,
|
||||
timeInterval: null,
|
||||
attachments: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function() {
|
||||
return this.$route.params.folder;
|
||||
},
|
||||
selectedFile: function() {
|
||||
return this.$route.params.file;
|
||||
},
|
||||
filePath: function() {
|
||||
const fileName = decode(this.selectedFile)
|
||||
return `.r2-explorer/emails/${this.selectedFolder}/${fileName}`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.$router.push({ name: `email-folder`, params: { bucket: newVal, folder: encode(this.selectedFolder) }})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
timeSince,
|
||||
contentFinishedLoading() {
|
||||
clearInterval(this.timeInterval)
|
||||
this.timeInterval = null
|
||||
name: "EmailFolderPage",
|
||||
data: () => ({
|
||||
srcdoc: null,
|
||||
file: null,
|
||||
fileHead: null,
|
||||
timeInterval: null,
|
||||
attachments: [],
|
||||
}),
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
return this.$route.params.folder;
|
||||
},
|
||||
selectedFile: function () {
|
||||
return this.$route.params.file;
|
||||
},
|
||||
filePath: function () {
|
||||
const fileName = decode(this.selectedFile);
|
||||
return `.r2-explorer/emails/${this.selectedFolder}/${fileName}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.$router.push({
|
||||
name: "email-folder",
|
||||
params: { bucket: newVal, folder: encode(this.selectedFolder) },
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
timeSince,
|
||||
contentFinishedLoading() {
|
||||
clearInterval(this.timeInterval);
|
||||
this.timeInterval = null;
|
||||
|
||||
this.resizeIframe()
|
||||
},
|
||||
resizeIframe() {
|
||||
if (this.$refs.renderWindow) {
|
||||
this.$refs.renderWindow.style.height = this.$refs.renderWindow.contentWindow.document.documentElement.scrollHeight + "px";
|
||||
}
|
||||
},
|
||||
this.resizeIframe();
|
||||
},
|
||||
resizeIframe() {
|
||||
if (this.$refs.renderWindow) {
|
||||
this.$refs.renderWindow.style.height = `${
|
||||
this.$refs.renderWindow.contentWindow.document.documentElement
|
||||
.scrollHeight
|
||||
}px`;
|
||||
}
|
||||
},
|
||||
|
||||
fetchEmail: async function() {
|
||||
const self = this
|
||||
const fileName = decode(this.selectedFile)
|
||||
fetchEmail: async function () {
|
||||
const fileName = decode(this.selectedFile);
|
||||
|
||||
const fileData = await apiHandler.downloadFile(this.selectedBucket, this.filePath, {})
|
||||
const fileData = await apiHandler.downloadFile(
|
||||
this.selectedBucket,
|
||||
this.filePath,
|
||||
{},
|
||||
);
|
||||
|
||||
const filename = fileName.split(".json")[0];
|
||||
const filename = fileName.split(".json")[0];
|
||||
|
||||
this.file = fileData.data;
|
||||
let htmlContent = fileData.data.html
|
||||
this.file = fileData.data;
|
||||
let htmlContent = fileData.data.html;
|
||||
|
||||
if (htmlContent) {
|
||||
// Add target blank to all links
|
||||
htmlContent = htmlContent.replaceAll(/<a(.*?)>(.*?)<\/a>/gi, '<a$1 target="_blank">$2</a>');
|
||||
if (htmlContent) {
|
||||
// Add target blank to all links
|
||||
htmlContent = htmlContent.replaceAll(
|
||||
/<a(.*?)>(.*?)<\/a>/gi,
|
||||
'<a$1 target="_blank">$2</a>',
|
||||
);
|
||||
|
||||
// Inject attachment url and replace in html to point to correct path
|
||||
for (const att of fileData.data.attachments) {
|
||||
att.display = true
|
||||
att.downloadUrl = `${this.mainStore.serverUrl}/api/buckets/${this.selectedBucket}/${encode(`.r2-explorer/emails/${this.selectedFolder}/${filename}/${att.filename}`)}`;
|
||||
// Inject attachment url and replace in html to point to correct path
|
||||
for (const att of fileData.data.attachments) {
|
||||
att.display = true;
|
||||
att.downloadUrl = `${this.mainStore.serverUrl}/api/buckets/${this.selectedBucket}/${encode(`.r2-explorer/emails/${this.selectedFolder}/${filename}/${att.filename}`)}`;
|
||||
|
||||
let contentId = att.contentId
|
||||
if (contentId){
|
||||
if (contentId.startsWith('<') && contentId.endsWith('>')){
|
||||
contentId = contentId.substring(1, contentId.length-1);
|
||||
}
|
||||
let contentId = att.contentId;
|
||||
if (contentId) {
|
||||
if (contentId.startsWith("<") && contentId.endsWith(">")) {
|
||||
contentId = contentId.substring(1, contentId.length - 1);
|
||||
}
|
||||
|
||||
const matchString = `cid:${contentId}`
|
||||
if (htmlContent.includes(matchString)) {
|
||||
htmlContent = htmlContent.replaceAll(`cid:${contentId}`, att.downloadUrl)
|
||||
att.display = false
|
||||
}
|
||||
}
|
||||
}
|
||||
const matchString = `cid:${contentId}`;
|
||||
if (htmlContent.includes(matchString)) {
|
||||
htmlContent = htmlContent.replaceAll(
|
||||
`cid:${contentId}`,
|
||||
att.downloadUrl,
|
||||
);
|
||||
att.display = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.srcdoc = htmlContent
|
||||
}
|
||||
this.srcdoc = htmlContent;
|
||||
}
|
||||
|
||||
this.attachments = fileData.data.attachments.filter((obj) => obj.display)
|
||||
this.attachments = fileData.data.attachments.filter((obj) => obj.display);
|
||||
|
||||
apiHandler.headFile(this.selectedBucket, this.filePath).then(async (obj) => {
|
||||
if (obj.customMetadata.read === 'false') {
|
||||
self.fileHead = await apiHandler.updateMetadata(self.selectedBucket, self.filePath, {
|
||||
...obj.customMetadata,
|
||||
read: true
|
||||
});
|
||||
} else {
|
||||
self.fileHead = obj
|
||||
}
|
||||
})
|
||||
apiHandler
|
||||
.headFile(this.selectedBucket, this.filePath)
|
||||
.then(async (obj) => {
|
||||
if (obj.customMetadata.read === "false") {
|
||||
this.fileHead = await apiHandler.updateMetadata(
|
||||
this.selectedBucket,
|
||||
this.filePath,
|
||||
{
|
||||
...obj.customMetadata,
|
||||
read: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this.fileHead = obj;
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
self.contentFinishedLoading()
|
||||
}, 10000)
|
||||
setTimeout(() => {
|
||||
this.contentFinishedLoading();
|
||||
}, 10000);
|
||||
|
||||
this.timeInterval = setInterval(function() {
|
||||
self.resizeIframe()
|
||||
}, 400);
|
||||
},
|
||||
markAsUnread: async function () {
|
||||
this.fileHead = await apiHandler.updateMetadata(this.selectedBucket, this.filePath, {
|
||||
...this.fileHead.customMetadata,
|
||||
read: false
|
||||
});
|
||||
this.timeInterval = setInterval(() => {
|
||||
this.resizeIframe();
|
||||
}, 400);
|
||||
},
|
||||
markAsUnread: async function () {
|
||||
this.fileHead = await apiHandler.updateMetadata(
|
||||
this.selectedBucket,
|
||||
this.filePath,
|
||||
{
|
||||
...this.fileHead.customMetadata,
|
||||
read: false,
|
||||
},
|
||||
);
|
||||
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: 'Email marked as unread!',
|
||||
timeout: 2500 // we will timeout it in 2.5s
|
||||
})
|
||||
},
|
||||
markAsRead: async function () {
|
||||
this.fileHead = await apiHandler.updateMetadata(this.selectedBucket, this.filePath, {
|
||||
...this.fileHead.customMetadata,
|
||||
read: true
|
||||
});
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: "Email marked as unread!",
|
||||
timeout: 2500, // we will timeout it in 2.5s
|
||||
});
|
||||
},
|
||||
markAsRead: async function () {
|
||||
this.fileHead = await apiHandler.updateMetadata(
|
||||
this.selectedBucket,
|
||||
this.filePath,
|
||||
{
|
||||
...this.fileHead.customMetadata,
|
||||
read: true,
|
||||
},
|
||||
);
|
||||
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: 'done', // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: 'Email marked as read!',
|
||||
timeout: 2500 // we will timeout it in 2.5s
|
||||
})
|
||||
},
|
||||
downloadAtt: function(attachment) {
|
||||
console.log(attachment)
|
||||
// return
|
||||
const link = document.createElement('a')
|
||||
link.download = attachment.filename
|
||||
this.q.notify({
|
||||
group: false,
|
||||
icon: "done", // we add an icon
|
||||
spinner: false, // we reset the spinner setting so the icon can be displayed
|
||||
message: "Email marked as read!",
|
||||
timeout: 2500, // we will timeout it in 2.5s
|
||||
});
|
||||
},
|
||||
downloadAtt: (attachment) => {
|
||||
console.log(attachment);
|
||||
// return
|
||||
const link = document.createElement("a");
|
||||
link.download = attachment.filename;
|
||||
|
||||
link.href = attachment.downloadUrl
|
||||
link.href = attachment.downloadUrl;
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchEmail();
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar()
|
||||
};
|
||||
}
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchEmail();
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -103,220 +103,227 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { api } from "boot/axios";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
import { apiHandler, encode, timeSince } from "../../appUtils";
|
||||
|
||||
export default defineComponent({
|
||||
name: "EmailFolderPage",
|
||||
data: function() {
|
||||
return {
|
||||
timeInterval: null,
|
||||
indexCursors: null,
|
||||
loading: false,
|
||||
loadMoreAutomatically: true,
|
||||
hasMorePages: true,
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
name: "sender",
|
||||
required: true,
|
||||
field: "sender",
|
||||
align: "left",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: "subject",
|
||||
required: true,
|
||||
field: "subject",
|
||||
align: "left",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: "lastModified",
|
||||
required: true,
|
||||
align: "left",
|
||||
field: "lastModified",
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
name: "has_attachments",
|
||||
required: true,
|
||||
align: "left",
|
||||
field: "has_attachments",
|
||||
sortable: false
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function() {
|
||||
return "inbox";
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.fetchFiles();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowClass: function(prop) {
|
||||
return prop.row.customMetadata.read === "true" ? "email-read" : "email-unread";
|
||||
},
|
||||
rowClick: function(evt, row, index) {
|
||||
const file = row.key.replace(/^.*[\\/]/, "");
|
||||
// const folder = row.key.replace(file, '')
|
||||
name: "EmailFolderPage",
|
||||
data: () => ({
|
||||
timeInterval: null,
|
||||
indexCursors: null,
|
||||
loading: false,
|
||||
loadMoreAutomatically: true,
|
||||
hasMorePages: true,
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
name: "sender",
|
||||
required: true,
|
||||
field: "sender",
|
||||
align: "left",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
name: "subject",
|
||||
required: true,
|
||||
field: "subject",
|
||||
align: "left",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
name: "lastModified",
|
||||
required: true,
|
||||
align: "left",
|
||||
field: "lastModified",
|
||||
sortable: false,
|
||||
},
|
||||
{
|
||||
name: "has_attachments",
|
||||
required: true,
|
||||
align: "left",
|
||||
field: "has_attachments",
|
||||
sortable: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: () => "inbox",
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.fetchFiles();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
rowClass: (prop) =>
|
||||
prop.row.customMetadata.read === "true" ? "email-read" : "email-unread",
|
||||
rowClick: function (evt, row, index) {
|
||||
const file = row.key.replace(/^.*[\\/]/, "");
|
||||
// const folder = row.key.replace(file, '')
|
||||
|
||||
this.$router.push({
|
||||
name: `email-file`,
|
||||
params: { bucket: this.selectedBucket, folder: this.selectedFolder, file: encode(file) }
|
||||
});
|
||||
},
|
||||
createOrUpdateIndex: async function(currentIndex) {
|
||||
let truncated = true;
|
||||
this.$router.push({
|
||||
name: "email-file",
|
||||
params: {
|
||||
bucket: this.selectedBucket,
|
||||
folder: this.selectedFolder,
|
||||
file: encode(file),
|
||||
},
|
||||
});
|
||||
},
|
||||
createOrUpdateIndex: async function (currentIndex) {
|
||||
let truncated = true;
|
||||
|
||||
// fallback when there's no previous index
|
||||
let cursor = null;
|
||||
let pageNum = 0;
|
||||
let indexData = {
|
||||
version: 1,
|
||||
cursors: []
|
||||
};
|
||||
// fallback when there's no previous index
|
||||
let cursor = null;
|
||||
let pageNum = 0;
|
||||
let indexData = {
|
||||
version: 1,
|
||||
cursors: [],
|
||||
};
|
||||
|
||||
if (currentIndex) {
|
||||
// There is a previous index
|
||||
indexData = currentIndex;
|
||||
if (currentIndex.cursors.length > 0) {
|
||||
// Pop last page, as its going to be updated
|
||||
const lastPage = currentIndex.cursors.pop();
|
||||
pageNum = lastPage.page;
|
||||
cursor = lastPage.cursor;
|
||||
}
|
||||
}
|
||||
if (currentIndex) {
|
||||
// There is a previous index
|
||||
indexData = currentIndex;
|
||||
if (currentIndex.cursors.length > 0) {
|
||||
// Pop last page, as its going to be updated
|
||||
const lastPage = currentIndex.cursors.pop();
|
||||
pageNum = lastPage.page;
|
||||
cursor = lastPage.cursor;
|
||||
}
|
||||
}
|
||||
|
||||
while (truncated) {
|
||||
console.log(`Updating index page ${pageNum}`);
|
||||
const response = await api.get(`/buckets/${this.selectedBucket}?include=customMetadata&include=httpMetadata`, {
|
||||
params: {
|
||||
delimiter: "/",
|
||||
prefix: encode(`.r2-explorer/emails/${this.selectedFolder}/`),
|
||||
cursor: cursor
|
||||
}
|
||||
});
|
||||
while (truncated) {
|
||||
console.log(`Updating index page ${pageNum}`);
|
||||
const response = await api.get(
|
||||
`/buckets/${this.selectedBucket}?include=customMetadata&include=httpMetadata`,
|
||||
{
|
||||
params: {
|
||||
delimiter: "/",
|
||||
prefix: encode(`.r2-explorer/emails/${this.selectedFolder}/`),
|
||||
cursor: cursor,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
indexData.cursors.push({
|
||||
page: pageNum,
|
||||
cursor: cursor,
|
||||
items: response.data.objects.length
|
||||
});
|
||||
indexData.cursors.push({
|
||||
page: pageNum,
|
||||
cursor: cursor,
|
||||
items: response.data.objects.length,
|
||||
});
|
||||
|
||||
// update cursor for next page
|
||||
cursor = response.data.cursor;
|
||||
truncated = response.data.truncated;
|
||||
pageNum++;
|
||||
}
|
||||
// update cursor for next page
|
||||
cursor = response.data.cursor;
|
||||
truncated = response.data.truncated;
|
||||
pageNum++;
|
||||
}
|
||||
|
||||
return indexData;
|
||||
},
|
||||
getOrCreateIndex: async function() {
|
||||
const indexKey = `.r2-explorer/emails/index-${this.selectedFolder}.json`;
|
||||
const fileData = await apiHandler.downloadFile(this.selectedBucket, indexKey, {}).then((obj) => obj.data).catch((obj) => null);
|
||||
return indexData;
|
||||
},
|
||||
getOrCreateIndex: async function () {
|
||||
const indexKey = `.r2-explorer/emails/index-${this.selectedFolder}.json`;
|
||||
const fileData = await apiHandler
|
||||
.downloadFile(this.selectedBucket, indexKey, {})
|
||||
.then((obj) => obj.data)
|
||||
.catch((obj) => null);
|
||||
|
||||
const updatedIndex = await this.createOrUpdateIndex(fileData);
|
||||
const updatedIndex = await this.createOrUpdateIndex(fileData);
|
||||
|
||||
const blob = new Blob([JSON.stringify(updatedIndex)], {
|
||||
type: "application/json"
|
||||
});
|
||||
try {
|
||||
await apiHandler.uploadObjects(blob, indexKey, this.selectedBucket);
|
||||
} catch (e) {}
|
||||
const blob = new Blob([JSON.stringify(updatedIndex)], {
|
||||
type: "application/json",
|
||||
});
|
||||
try {
|
||||
await apiHandler.uploadObjects(blob, indexKey, this.selectedBucket);
|
||||
} catch (e) {}
|
||||
|
||||
return updatedIndex;
|
||||
},
|
||||
loadNextPage: async function(index, done) {
|
||||
const page = this.indexCursors[index];
|
||||
return updatedIndex;
|
||||
},
|
||||
loadNextPage: async function (index, done) {
|
||||
const page = this.indexCursors[index];
|
||||
|
||||
if (page) {
|
||||
await this.loadIndexPage(page);
|
||||
} else {
|
||||
// No more pages to load
|
||||
this.loadMoreAutomatically = true;
|
||||
this.hasMorePages = false;
|
||||
}
|
||||
if (page) {
|
||||
await this.loadIndexPage(page);
|
||||
} else {
|
||||
// No more pages to load
|
||||
this.loadMoreAutomatically = true;
|
||||
this.hasMorePages = false;
|
||||
}
|
||||
|
||||
done();
|
||||
},
|
||||
fetchFiles: async function() {
|
||||
this.loading = true;
|
||||
this.rows = []
|
||||
done();
|
||||
},
|
||||
fetchFiles: async function () {
|
||||
this.loading = true;
|
||||
this.rows = [];
|
||||
|
||||
const indexData = await this.getOrCreateIndex();
|
||||
const indexData = await this.getOrCreateIndex();
|
||||
|
||||
this.indexCursors = indexData.cursors.reverse();
|
||||
this.indexCursors = indexData.cursors.reverse();
|
||||
|
||||
await this.loadNextPage(0, () => {
|
||||
});
|
||||
await this.$refs.infScroll.setIndex(0); // First page is 0
|
||||
await this.$refs.infScroll.poll();
|
||||
await this.loadNextPage(0, () => {});
|
||||
await this.$refs.infScroll.setIndex(0); // First page is 0
|
||||
await this.$refs.infScroll.poll();
|
||||
|
||||
this.loadMoreAutomatically = false;
|
||||
this.loadMoreAutomatically = false;
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
loadIndexPage: async function(page) {
|
||||
const response = await apiHandler.listObjects(
|
||||
this.selectedBucket,
|
||||
`.r2-explorer/emails/${this.selectedFolder}/`,
|
||||
"/",
|
||||
page.cursor
|
||||
);
|
||||
this.loading = false;
|
||||
},
|
||||
loadIndexPage: async function (page) {
|
||||
const response = await apiHandler.listObjects(
|
||||
this.selectedBucket,
|
||||
`.r2-explorer/emails/${this.selectedFolder}/`,
|
||||
"/",
|
||||
page.cursor,
|
||||
);
|
||||
|
||||
if (response.data.objects) {
|
||||
const files = response.data.objects.filter(function(obj) {
|
||||
return !obj.key.endsWith("/"); // Remove selected folder
|
||||
}).map(function(obj) {
|
||||
const date = new Date(parseInt(obj.customMetadata.timestamp));
|
||||
if (response.data.objects) {
|
||||
const files = response.data.objects
|
||||
.filter((obj) => {
|
||||
return !obj.key.endsWith("/"); // Remove selected folder
|
||||
})
|
||||
.map((obj) => {
|
||||
const date = new Date(
|
||||
Number.parseInt(obj.customMetadata.timestamp),
|
||||
);
|
||||
|
||||
return {
|
||||
...obj,
|
||||
sender: obj.customMetadata.from_name || obj.customMetadata.from_address,
|
||||
subject: obj.customMetadata.subject,
|
||||
has_attachments: obj.customMetadata.has_attachments === "true",
|
||||
read: obj.customMetadata.read,
|
||||
lastModified: timeSince(date),
|
||||
timestamp: parseInt(obj.customMetadata.timestamp)
|
||||
};
|
||||
});
|
||||
return {
|
||||
...obj,
|
||||
sender:
|
||||
obj.customMetadata.from_name || obj.customMetadata.from_address,
|
||||
subject: obj.customMetadata.subject,
|
||||
has_attachments: obj.customMetadata.has_attachments === "true",
|
||||
read: obj.customMetadata.read,
|
||||
lastModified: timeSince(date),
|
||||
timestamp: Number.parseInt(obj.customMetadata.timestamp),
|
||||
};
|
||||
});
|
||||
|
||||
for (const f of files.reverse()) {
|
||||
this.rows.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
unmounted () {
|
||||
clearInterval(this.timeInterval)
|
||||
this.timeInterval = null
|
||||
},
|
||||
mounted () {
|
||||
const self = this
|
||||
|
||||
this.timeInterval = setInterval(() => {
|
||||
self.fetchFiles()
|
||||
}, 300000) // 5 minutes
|
||||
},
|
||||
created() {
|
||||
this.fetchFiles();
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore()
|
||||
};
|
||||
}
|
||||
for (const f of files.reverse()) {
|
||||
this.rows.push(f);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.timeInterval);
|
||||
this.timeInterval = null;
|
||||
},
|
||||
mounted() {
|
||||
this.timeInterval = setInterval(() => {
|
||||
this.fetchFiles();
|
||||
}, 300000); // 5 minutes
|
||||
},
|
||||
created() {
|
||||
this.fetchFiles();
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,88 +18,97 @@
|
||||
</q-list>
|
||||
</template>
|
||||
<script>
|
||||
import { decode, encode, ROOT_FOLDER } from "src/appUtils";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { useQuasar } from "quasar";
|
||||
import { ROOT_FOLDER, decode, encode } from "src/appUtils";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
|
||||
export default {
|
||||
name: "FileContextMenu",
|
||||
props: {
|
||||
prop: {},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function() {
|
||||
return this.$route.params.bucket
|
||||
},
|
||||
selectedFolder: function() {
|
||||
if (this.$route.params.folder && this.$route.params.folder !== ROOT_FOLDER) {
|
||||
return decode(this.$route.params.folder)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renameObject: function() {
|
||||
this.$emit('renameObject', this.prop.row)
|
||||
},
|
||||
openObject: function() {
|
||||
this.$emit('openObject', this.prop.row)
|
||||
},
|
||||
deleteObject: function() {
|
||||
this.$emit('deleteObject', this.prop.row)
|
||||
},
|
||||
shareObject: async function() {
|
||||
let url
|
||||
if (this.prop.row.type === 'folder') {
|
||||
url = window.location.origin + this.$router.resolve({
|
||||
name: 'files-folder',
|
||||
params: {
|
||||
bucket: this.selectedBucket,
|
||||
folder: encode(this.prop.row.key)
|
||||
}
|
||||
}).href
|
||||
} else {
|
||||
url = window.location.origin + this.$router.resolve({
|
||||
name: 'files-file',
|
||||
params: {
|
||||
bucket: this.selectedBucket,
|
||||
folder: this.selectedFolder ? encode(this.selectedFolder) : ROOT_FOLDER,
|
||||
file: this.prop.row.nameHash
|
||||
}
|
||||
}).href
|
||||
}
|
||||
name: "FileContextMenu",
|
||||
props: {
|
||||
prop: {},
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (
|
||||
this.$route.params.folder &&
|
||||
this.$route.params.folder !== ROOT_FOLDER
|
||||
) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renameObject: function () {
|
||||
this.$emit("renameObject", this.prop.row);
|
||||
},
|
||||
openObject: function () {
|
||||
this.$emit("openObject", this.prop.row);
|
||||
},
|
||||
deleteObject: function () {
|
||||
this.$emit("deleteObject", this.prop.row);
|
||||
},
|
||||
shareObject: async function () {
|
||||
let url;
|
||||
if (this.prop.row.type === "folder") {
|
||||
url =
|
||||
window.location.origin +
|
||||
this.$router.resolve({
|
||||
name: "files-folder",
|
||||
params: {
|
||||
bucket: this.selectedBucket,
|
||||
folder: encode(this.prop.row.key),
|
||||
},
|
||||
}).href;
|
||||
} else {
|
||||
url =
|
||||
window.location.origin +
|
||||
this.$router.resolve({
|
||||
name: "files-file",
|
||||
params: {
|
||||
bucket: this.selectedBucket,
|
||||
folder: this.selectedFolder
|
||||
? encode(this.selectedFolder)
|
||||
: ROOT_FOLDER,
|
||||
file: this.prop.row.nameHash,
|
||||
},
|
||||
}).href;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
this.q.notify({
|
||||
message: 'Link to file copied to clipboard!',
|
||||
timeout: 5000,
|
||||
type: 'positive',
|
||||
})
|
||||
} catch (err) {
|
||||
this.q.notify({
|
||||
message: 'Failed to copy: ' + err,
|
||||
timeout: 5000,
|
||||
type: 'negative',
|
||||
})
|
||||
}
|
||||
},
|
||||
downloadObject: function() {
|
||||
const link = document.createElement('a')
|
||||
link.download = this.prop.row.name
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
this.q.notify({
|
||||
message: "Link to file copied to clipboard!",
|
||||
timeout: 5000,
|
||||
type: "positive",
|
||||
});
|
||||
} catch (err) {
|
||||
this.q.notify({
|
||||
message: `Failed to copy: ${err}`,
|
||||
timeout: 5000,
|
||||
type: "negative",
|
||||
});
|
||||
}
|
||||
},
|
||||
downloadObject: function () {
|
||||
const link = document.createElement("a");
|
||||
link.download = this.prop.row.name;
|
||||
|
||||
link.href = `${this.mainStore.serverUrl}/api/buckets/${this.selectedBucket}/${encode(this.prop.row.key)}`
|
||||
link.href = `${this.mainStore.serverUrl}/api/buckets/${this.selectedBucket}/${encode(this.prop.row.key)}`;
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar()
|
||||
}
|
||||
},
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar(),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -79,192 +79,215 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { apiHandler, decode, encode, ROOT_FOLDER } from "../../appUtils";
|
||||
import FileOptions from "components/files/FileOptions.vue";
|
||||
import FilePreview from "components/preview/FilePreview.vue";
|
||||
import DragAndDrop from "components/utils/DragAndDrop.vue";
|
||||
import FileOptions from "components/files/FileOptions.vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import FileContextMenu from "pages/files/FileContextMenu.vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
import { defineComponent } from "vue";
|
||||
import { ROOT_FOLDER, apiHandler, decode, encode } from "../../appUtils";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FilesIndexPage',
|
||||
components: { FileContextMenu, FileOptions, DragAndDrop, FilePreview },
|
||||
data: function () {
|
||||
return {
|
||||
loading: false,
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
label: 'Name',
|
||||
align: 'left',
|
||||
field: 'name',
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => {
|
||||
if (rowA.type === 'folder') {
|
||||
if (rowB.type === 'folder') {
|
||||
// both are folders
|
||||
return a.localeCompare(b)
|
||||
} else {
|
||||
// only first is folder
|
||||
return 1
|
||||
}
|
||||
} else if (rowB.type === 'folder') {
|
||||
// only second is folder
|
||||
return -1
|
||||
} else {
|
||||
// none is folder
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lastModified',
|
||||
required: true,
|
||||
label: 'Last Modified',
|
||||
align: 'left',
|
||||
field: 'lastModified',
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => {
|
||||
return rowA.timestamp - rowB.timestamp
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
required: true,
|
||||
label: 'Size',
|
||||
align: 'left',
|
||||
field: 'size',
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => {
|
||||
return rowA.sizeRaw - rowB.sizeRaw
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
label: '',
|
||||
sortable: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (this.$route.params.folder && this.$route.params.folder !== ROOT_FOLDER) {
|
||||
return decode(this.$route.params.folder)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
breadcrumbs: function () {
|
||||
if (this.selectedFolder) {
|
||||
return [{
|
||||
name: this.selectedBucket,
|
||||
path: '/'
|
||||
}, ...this.selectedFolder.split('/')
|
||||
.filter((obj) => obj !== '')
|
||||
.map((item, index, arr) => {
|
||||
return {
|
||||
name: item,
|
||||
path: arr.slice(0, index + 1).join('/').replace('Home/', '') + '/'
|
||||
}
|
||||
})
|
||||
]
|
||||
} else {
|
||||
return [{
|
||||
name: this.selectedBucket,
|
||||
path: '/'
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.fetchFiles()
|
||||
},
|
||||
selectedFolder(newVal) {
|
||||
this.fetchFiles()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openRowClick: function(evt, row, index) {
|
||||
evt.preventDefault()
|
||||
this.openObject(row)
|
||||
},
|
||||
openRowDlbClick: function(evt, row, index) {
|
||||
evt.preventDefault()
|
||||
this.$bus.emit("openFileDetails", row);
|
||||
},
|
||||
breadcrumbsClick: function(obj) {
|
||||
this.$router.push({ name: `files-folder`, params: { bucket: this.selectedBucket, folder: encode(obj.path) }})
|
||||
},
|
||||
rowClick: function(evt, row) {
|
||||
if (row.type === 'folder') {
|
||||
this.$router.push({ name: `files-folder`, params: { bucket: this.selectedBucket, folder: encode(row.key) }})
|
||||
} else {
|
||||
// console.log(row)
|
||||
this.$refs.preview.openFile(row)
|
||||
}
|
||||
},
|
||||
openObject: function(row) {
|
||||
if (row.type === 'folder') {
|
||||
this.$router.push({ name: `files-folder`, params: { bucket: this.selectedBucket, folder: encode(row.key) }})
|
||||
} else {
|
||||
// console.log(row)
|
||||
this.$refs.preview.openFile(row)
|
||||
}
|
||||
},
|
||||
renameObject: function(row) {
|
||||
if (row.type === 'folder') {
|
||||
this.$router.push({ name: `files-folder`, params: { bucket: this.selectedBucket, folder: encode(row.key) }})
|
||||
} else {
|
||||
// console.log(row)
|
||||
this.$refs.preview.openFile(row)
|
||||
}
|
||||
},
|
||||
fetchFiles: async function () {
|
||||
this.loading = true
|
||||
name: "FilesIndexPage",
|
||||
components: { FileContextMenu, FileOptions, DragAndDrop, FilePreview },
|
||||
data: () => ({
|
||||
loading: false,
|
||||
rows: [],
|
||||
columns: [
|
||||
{
|
||||
name: "name",
|
||||
required: true,
|
||||
label: "Name",
|
||||
align: "left",
|
||||
field: "name",
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => {
|
||||
if (rowA.type === "folder") {
|
||||
if (rowB.type === "folder") {
|
||||
// both are folders
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
// only first is folder
|
||||
return 1;
|
||||
}
|
||||
if (rowB.type === "folder") {
|
||||
// only second is folder
|
||||
return -1;
|
||||
}
|
||||
// none is folder
|
||||
return a.localeCompare(b);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "lastModified",
|
||||
required: true,
|
||||
label: "Last Modified",
|
||||
align: "left",
|
||||
field: "lastModified",
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => {
|
||||
return rowA.timestamp - rowB.timestamp;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "size",
|
||||
required: true,
|
||||
label: "Size",
|
||||
align: "left",
|
||||
field: "size",
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => {
|
||||
return rowA.sizeRaw - rowB.sizeRaw;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "options",
|
||||
label: "",
|
||||
sortable: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
computed: {
|
||||
selectedBucket: function () {
|
||||
return this.$route.params.bucket;
|
||||
},
|
||||
selectedFolder: function () {
|
||||
if (
|
||||
this.$route.params.folder &&
|
||||
this.$route.params.folder !== ROOT_FOLDER
|
||||
) {
|
||||
return decode(this.$route.params.folder);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
breadcrumbs: function () {
|
||||
if (this.selectedFolder) {
|
||||
return [
|
||||
{
|
||||
name: this.selectedBucket,
|
||||
path: "/",
|
||||
},
|
||||
...this.selectedFolder
|
||||
.split("/")
|
||||
.filter((obj) => obj !== "")
|
||||
.map((item, index, arr) => {
|
||||
return {
|
||||
name: item,
|
||||
path: `${arr
|
||||
.slice(0, index + 1)
|
||||
.join("/")
|
||||
.replace("Home/", "")}/`,
|
||||
};
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
name: this.selectedBucket,
|
||||
path: "/",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedBucket(newVal) {
|
||||
this.fetchFiles();
|
||||
},
|
||||
selectedFolder(newVal) {
|
||||
this.fetchFiles();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openRowClick: function (evt, row, index) {
|
||||
evt.preventDefault();
|
||||
this.openObject(row);
|
||||
},
|
||||
openRowDlbClick: function (evt, row, index) {
|
||||
evt.preventDefault();
|
||||
this.$bus.emit("openFileDetails", row);
|
||||
},
|
||||
breadcrumbsClick: function (obj) {
|
||||
this.$router.push({
|
||||
name: "files-folder",
|
||||
params: { bucket: this.selectedBucket, folder: encode(obj.path) },
|
||||
});
|
||||
},
|
||||
rowClick: function (evt, row) {
|
||||
if (row.type === "folder") {
|
||||
this.$router.push({
|
||||
name: "files-folder",
|
||||
params: { bucket: this.selectedBucket, folder: encode(row.key) },
|
||||
});
|
||||
} else {
|
||||
// console.log(row)
|
||||
this.$refs.preview.openFile(row);
|
||||
}
|
||||
},
|
||||
openObject: function (row) {
|
||||
if (row.type === "folder") {
|
||||
this.$router.push({
|
||||
name: "files-folder",
|
||||
params: { bucket: this.selectedBucket, folder: encode(row.key) },
|
||||
});
|
||||
} else {
|
||||
// console.log(row)
|
||||
this.$refs.preview.openFile(row);
|
||||
}
|
||||
},
|
||||
renameObject: function (row) {
|
||||
if (row.type === "folder") {
|
||||
this.$router.push({
|
||||
name: "files-folder",
|
||||
params: { bucket: this.selectedBucket, folder: encode(row.key) },
|
||||
});
|
||||
} else {
|
||||
// console.log(row)
|
||||
this.$refs.preview.openFile(row);
|
||||
}
|
||||
},
|
||||
fetchFiles: async function () {
|
||||
this.loading = true;
|
||||
|
||||
this.rows = await apiHandler.fetchFile(this.selectedBucket, this.selectedFolder, '/')
|
||||
this.loading = false
|
||||
},
|
||||
openPreviewFromKey: async function () {
|
||||
let key = `${decode(this.$route.params.file)}`
|
||||
if (this.selectedFolder && this.selectedFolder !== ROOT_FOLDER) {
|
||||
key = `${this.selectedFolder}${decode(this.$route.params.file)}`
|
||||
}
|
||||
this.rows = await apiHandler.fetchFile(
|
||||
this.selectedBucket,
|
||||
this.selectedFolder,
|
||||
"/",
|
||||
);
|
||||
this.loading = false;
|
||||
},
|
||||
openPreviewFromKey: async function () {
|
||||
let key = `${decode(this.$route.params.file)}`;
|
||||
if (this.selectedFolder && this.selectedFolder !== ROOT_FOLDER) {
|
||||
key = `${this.selectedFolder}${decode(this.$route.params.file)}`;
|
||||
}
|
||||
|
||||
const file = await apiHandler.headFile(this.selectedBucket, key)
|
||||
this.$refs.preview.openFile(file)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchFiles()
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.table.sort('name')
|
||||
const file = await apiHandler.headFile(this.selectedBucket, key);
|
||||
this.$refs.preview.openFile(file);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchFiles();
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.table.sort("name");
|
||||
|
||||
this.$bus.on('fetchFiles', this.fetchFiles)
|
||||
this.$bus.on("fetchFiles", this.fetchFiles);
|
||||
|
||||
if (this.$route.params.file) {
|
||||
this.openPreviewFromKey()
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$bus.off('fetchFiles')
|
||||
},
|
||||
setup () {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar()
|
||||
}
|
||||
},
|
||||
})
|
||||
if (this.$route.params.file) {
|
||||
this.openPreviewFromKey();
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.$bus.off("fetchFiles");
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
q: useQuasar(),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/*** Regex Markdown Parser by chalarangelo ***/
|
||||
// Replaces 'regex' with 'replacement' in 'str'
|
||||
// Curry function, usage: replaceRegex(regexVar, replacementVar) (strVar)
|
||||
const replaceRegex = function(regex, replacement){
|
||||
return function(str){
|
||||
return str.replace(regex, replacement);
|
||||
}
|
||||
}
|
||||
const replaceRegex = (regex, replacement) => (str) =>
|
||||
str.replace(regex, replacement);
|
||||
// Regular expressions for Markdown (a bit strict, but they work)
|
||||
const codeBlockRegex = /((\n\t)(.*))+/g;
|
||||
const inlineCodeRegex = /(`)(.*?)\1/g;
|
||||
@@ -18,48 +15,46 @@ const blockquoteRegex = /\n(>|\>)(.*)/g;
|
||||
const horizontalRuleRegex = /\n((\-{3,})|(={3,}))/g;
|
||||
const unorderedListRegex = /(\n\s*(\-|\+)\s.*)+/g;
|
||||
const orderedListRegex = /(\n\s*([0-9]+\.)\s.*)+/g;
|
||||
const paragraphRegex = /\n+(?!<pre>)(?!<h)(?!<ul>)(?!<blockquote)(?!<hr)(?!\t)([^\n]+)\n/g;
|
||||
const paragraphRegex =
|
||||
/\n+(?!<pre>)(?!<h)(?!<ul>)(?!<blockquote)(?!<hr)(?!\t)([^\n]+)\n/g;
|
||||
// Replacer functions for Markdown
|
||||
const codeBlockReplacer = function(fullMatch){
|
||||
return '\n<pre>' + fullMatch + '</pre>';
|
||||
}
|
||||
const inlineCodeReplacer = function(fullMatch, tagStart, tagContents){
|
||||
return '<code>' + tagContents + '</code>';
|
||||
}
|
||||
const imageReplacer = function(fullMatch, tagTitle, tagURL){
|
||||
return '<img src="' + tagURL + '" alt="' + tagTitle + '" />';
|
||||
}
|
||||
const linkReplacer = function(fullMatch, tagTitle, tagURL){
|
||||
return '<a href="' + tagURL + '">' + tagTitle + '</a>';
|
||||
}
|
||||
const headingReplacer = function(fullMatch, tagStart, tagContents){
|
||||
return '\n<h' + tagStart.trim().length + '>' + tagContents + '</h' + tagStart.trim().length + '>';
|
||||
}
|
||||
const boldItalicsReplacer = function(fullMatch, tagStart, tagContents){
|
||||
return '<' + ( (tagStart.trim().length==1)?('em'):('strong') ) + '>'+ tagContents + '</' + ( (tagStart.trim().length==1)?('em'):('strong') ) + '>';
|
||||
}
|
||||
const strikethroughReplacer = function(fullMatch, tagStart, tagContents){
|
||||
return '<del>' + tagContents + '</del>';
|
||||
}
|
||||
const blockquoteReplacer = function(fullMatch, tagStart, tagContents){
|
||||
return '\n<blockquote>' + tagContents + '</blockquote>';
|
||||
}
|
||||
const horizontalRuleReplacer = function(fullMatch){
|
||||
return '\n<hr />';
|
||||
}
|
||||
const unorderedListReplacer = function(fullMatch){
|
||||
let items = '';
|
||||
fullMatch.trim().split('\n').forEach( item => { items += '<li>' + item.substring(2) + '</li>'; } );
|
||||
return '\n<ul>' + items + '</ul>';
|
||||
}
|
||||
const orderedListReplacer = function(fullMatch){
|
||||
let items = '';
|
||||
fullMatch.trim().split('\n').forEach( item => { items += '<li>' + item.substring(item.indexOf('.')+2) + '</li>'; } );
|
||||
return '\n<ol>' + items + '</ol>';
|
||||
}
|
||||
const paragraphReplacer = function(fullMatch, tagContents){
|
||||
return '<p>' + tagContents + '</p>';
|
||||
}
|
||||
const codeBlockReplacer = (fullMatch) => `\n<pre>${fullMatch}</pre>`;
|
||||
const inlineCodeReplacer = (fullMatch, tagStart, tagContents) =>
|
||||
`<code>${tagContents}</code>`;
|
||||
const imageReplacer = (fullMatch, tagTitle, tagURL) =>
|
||||
`<img src="${tagURL}" alt="${tagTitle}" />`;
|
||||
const linkReplacer = (fullMatch, tagTitle, tagURL) =>
|
||||
`<a href="${tagURL}">${tagTitle}</a>`;
|
||||
const headingReplacer = (fullMatch, tagStart, tagContents) =>
|
||||
`\n<h${tagStart.trim().length}>${tagContents}</h${tagStart.trim().length}>`;
|
||||
const boldItalicsReplacer = (fullMatch, tagStart, tagContents) =>
|
||||
`<${tagStart.trim().length === 1 ? "em" : "strong"}>${tagContents}</${tagStart.trim().length === 1 ? "em" : "strong"}>`;
|
||||
const strikethroughReplacer = (fullMatch, tagStart, tagContents) =>
|
||||
`<del>${tagContents}</del>`;
|
||||
const blockquoteReplacer = (fullMatch, tagStart, tagContents) =>
|
||||
`\n<blockquote>${tagContents}</blockquote>`;
|
||||
const horizontalRuleReplacer = (fullMatch) => "\n<hr />";
|
||||
const unorderedListReplacer = (fullMatch) => {
|
||||
let items = "";
|
||||
fullMatch
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((item) => {
|
||||
items += `<li>${item.substring(2)}</li>`;
|
||||
});
|
||||
return `\n<ul>${items}</ul>`;
|
||||
};
|
||||
const orderedListReplacer = (fullMatch) => {
|
||||
let items = "";
|
||||
fullMatch
|
||||
.trim()
|
||||
.split("\n")
|
||||
.forEach((item) => {
|
||||
items += `<li>${item.substring(item.indexOf(".") + 2)}</li>`;
|
||||
});
|
||||
return `\n<ol>${items}</ol>`;
|
||||
};
|
||||
const paragraphReplacer = (fullMatch, tagContents) => `<p>${tagContents}</p>`;
|
||||
// Rules for Markdown parsing (use in order of appearance for best results)
|
||||
const replaceCodeBlocks = replaceRegex(codeBlockRegex, codeBlockReplacer);
|
||||
const replaceInlineCodes = replaceRegex(inlineCodeRegex, inlineCodeReplacer);
|
||||
@@ -67,33 +62,60 @@ const replaceImages = replaceRegex(imageRegex, imageReplacer);
|
||||
const replaceLinks = replaceRegex(linkRegex, linkReplacer);
|
||||
const replaceHeadings = replaceRegex(headingRegex, headingReplacer);
|
||||
const replaceBoldItalics = replaceRegex(boldItalicsRegex, boldItalicsReplacer);
|
||||
const replaceceStrikethrough = replaceRegex(strikethroughRegex, strikethroughReplacer);
|
||||
const replaceceStrikethrough = replaceRegex(
|
||||
strikethroughRegex,
|
||||
strikethroughReplacer,
|
||||
);
|
||||
const replaceBlockquotes = replaceRegex(blockquoteRegex, blockquoteReplacer);
|
||||
const replaceHorizontalRules = replaceRegex(horizontalRuleRegex, horizontalRuleReplacer);
|
||||
const replaceUnorderedLists = replaceRegex(unorderedListRegex, unorderedListReplacer);
|
||||
const replaceHorizontalRules = replaceRegex(
|
||||
horizontalRuleRegex,
|
||||
horizontalRuleReplacer,
|
||||
);
|
||||
const replaceUnorderedLists = replaceRegex(
|
||||
unorderedListRegex,
|
||||
unorderedListReplacer,
|
||||
);
|
||||
const replaceOrderedLists = replaceRegex(orderedListRegex, orderedListReplacer);
|
||||
const replaceParagraphs = replaceRegex(paragraphRegex, paragraphReplacer);
|
||||
// Fix for tab-indexed code blocks
|
||||
const codeBlockFixRegex = /\n(<pre>)((\n|.)*)(<\/pre>)/g;
|
||||
const codeBlockFixer = function(fullMatch, tagStart, tagContents, lastMatch, tagEnd){
|
||||
let lines = '';
|
||||
tagContents.split('\n').forEach( line => { lines += line.substring(1) + '\n'; } );
|
||||
const codeBlockFixer = (
|
||||
fullMatch,
|
||||
tagStart,
|
||||
tagContents,
|
||||
lastMatch,
|
||||
tagEnd,
|
||||
) => {
|
||||
let lines = "";
|
||||
tagContents.split("\n").forEach((line) => {
|
||||
lines += `${line.substring(1)}\n`;
|
||||
});
|
||||
return tagStart + lines + tagEnd;
|
||||
}
|
||||
};
|
||||
const fixCodeBlocks = replaceRegex(codeBlockFixRegex, codeBlockFixer);
|
||||
// Replacement rule order function for Markdown
|
||||
// Do not use as-is, prefer parseMarkdown as seen below
|
||||
const replaceMarkdown = function(str) {
|
||||
return replaceParagraphs(replaceOrderedLists(replaceUnorderedLists(
|
||||
replaceHorizontalRules(replaceBlockquotes(replaceceStrikethrough(
|
||||
replaceBoldItalics(replaceHeadings(replaceLinks(replaceImages(
|
||||
replaceInlineCodes(replaceCodeBlocks(str))
|
||||
))))
|
||||
)))
|
||||
)));
|
||||
}
|
||||
const replaceMarkdown = (str) =>
|
||||
replaceParagraphs(
|
||||
replaceOrderedLists(
|
||||
replaceUnorderedLists(
|
||||
replaceHorizontalRules(
|
||||
replaceBlockquotes(
|
||||
replaceceStrikethrough(
|
||||
replaceBoldItalics(
|
||||
replaceHeadings(
|
||||
replaceLinks(
|
||||
replaceImages(replaceInlineCodes(replaceCodeBlocks(str))),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
// Parser for Markdown (fixes code, adds empty lines around for parsing)
|
||||
// Usage: parseMarkdown(strVar)
|
||||
export const parseMarkdown = function(str) {
|
||||
return fixCodeBlocks(replaceMarkdown('\n' + str + '\n')).trim();
|
||||
}
|
||||
export const parseMarkdown = (str) =>
|
||||
fixCodeBlocks(replaceMarkdown(`\n${str}\n`)).trim();
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { route } from 'quasar/wrappers'
|
||||
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import routes from './routes'
|
||||
import { route } from "quasar/wrappers";
|
||||
import {
|
||||
createMemoryHistory,
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory,
|
||||
} from "vue-router";
|
||||
import routes from "./routes";
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
@@ -11,20 +16,22 @@ import routes from './routes'
|
||||
* with the Router instance.
|
||||
*/
|
||||
|
||||
export default route(function (/* { store, ssrContext } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
||||
export default route((/* { store, ssrContext } */) => {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: process.env.VUE_ROUTER_MODE === "history"
|
||||
? createWebHistory
|
||||
: createWebHashHistory;
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||
})
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||
});
|
||||
|
||||
return Router
|
||||
})
|
||||
return Router;
|
||||
});
|
||||
|
||||
@@ -1,86 +1,102 @@
|
||||
const routes = [
|
||||
{
|
||||
path: "/auth",
|
||||
component: () => import("layouts/AuthLayout.vue"),
|
||||
children: [
|
||||
{ path: "login", name: "login", component: () => import("pages/auth/LoginPage.vue") }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("layouts/MainLayout.vue"),
|
||||
children: [
|
||||
{ path: "/", name: "home", component: () => import("pages/HomePage.vue") },
|
||||
{ path: "/:bucket/files", name: "files-home", component: () => import("pages/files/FilesFolderPage.vue") },
|
||||
{
|
||||
path: "/:bucket/files/:folder",
|
||||
name: "files-folder",
|
||||
component: () => import("pages/files/FilesFolderPage.vue")
|
||||
},
|
||||
{
|
||||
path: "/:bucket/files/:folder/:file",
|
||||
name: "files-file",
|
||||
component: () => import("pages/files/FilesFolderPage.vue")
|
||||
},
|
||||
{
|
||||
path: "/auth",
|
||||
component: () => import("layouts/AuthLayout.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
name: "login",
|
||||
component: () => import("pages/auth/LoginPage.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: () => import("layouts/MainLayout.vue"),
|
||||
children: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("pages/HomePage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:bucket/files",
|
||||
name: "files-home",
|
||||
component: () => import("pages/files/FilesFolderPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:bucket/files/:folder",
|
||||
name: "files-folder",
|
||||
component: () => import("pages/files/FilesFolderPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:bucket/files/:folder/:file",
|
||||
name: "files-file",
|
||||
component: () => import("pages/files/FilesFolderPage.vue"),
|
||||
},
|
||||
|
||||
{ path: "/:bucket/email", name: "email-home", component: () => import("pages/email/EmailFolderPage.vue") },
|
||||
{
|
||||
path: "/:bucket/email/:folder",
|
||||
name: "email-folder",
|
||||
component: () => import("pages/email/EmailFolderPage.vue")
|
||||
},
|
||||
{
|
||||
path: "/:bucket/email/:folder/:file",
|
||||
name: "email-file",
|
||||
component: () => import("pages/email/EmailFilePage.vue")
|
||||
},
|
||||
{
|
||||
path: "/:bucket/email",
|
||||
name: "email-home",
|
||||
component: () => import("pages/email/EmailFolderPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:bucket/email/:folder",
|
||||
name: "email-folder",
|
||||
component: () => import("pages/email/EmailFolderPage.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:bucket/email/:folder/:file",
|
||||
name: "email-file",
|
||||
component: () => import("pages/email/EmailFilePage.vue"),
|
||||
},
|
||||
|
||||
// backwards compatibility
|
||||
{
|
||||
path: "/storage/:bucket",
|
||||
redirect: to => {
|
||||
return {
|
||||
name: "files-home",
|
||||
params: {
|
||||
bucket: to.params.bucket
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/storage/:bucket/:folder",
|
||||
redirect: to => {
|
||||
return {
|
||||
name: "files-folder",
|
||||
params: {
|
||||
bucket: to.params.bucket,
|
||||
folder: to.params.folder
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/storage/:bucket/:folder/:file",
|
||||
redirect: to => {
|
||||
return {
|
||||
name: "files-file",
|
||||
params: {
|
||||
bucket: to.params.bucket,
|
||||
folder: to.params.folder,
|
||||
file: to.params.file
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
// backwards compatibility
|
||||
{
|
||||
path: "/storage/:bucket",
|
||||
redirect: (to) => {
|
||||
return {
|
||||
name: "files-home",
|
||||
params: {
|
||||
bucket: to.params.bucket,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/storage/:bucket/:folder",
|
||||
redirect: (to) => {
|
||||
return {
|
||||
name: "files-folder",
|
||||
params: {
|
||||
bucket: to.params.bucket,
|
||||
folder: to.params.folder,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/storage/:bucket/:folder/:file",
|
||||
redirect: (to) => {
|
||||
return {
|
||||
name: "files-file",
|
||||
params: {
|
||||
bucket: to.params.bucket,
|
||||
folder: to.params.folder,
|
||||
file: to.params.file,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
{
|
||||
path: "/:catchAll(.*)*",
|
||||
component: () => import("pages/ErrorNotFound.vue")
|
||||
}
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
{
|
||||
path: "/:catchAll(.*)*",
|
||||
component: () => import("pages/ErrorNotFound.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
@@ -1,77 +1,80 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from 'boot/axios';
|
||||
import { api } from "boot/axios";
|
||||
import { defineStore } from "pinia";
|
||||
import { useMainStore } from "stores/main-store";
|
||||
|
||||
const SESSION_KEY = 'r2_explorer_session_token';
|
||||
const SESSION_KEY = "r2_explorer_session_token";
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: '',
|
||||
loginMethod: '',
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated: state => !!state.user,
|
||||
StateUser: state => state.user
|
||||
},
|
||||
actions: {
|
||||
async LogIn(router, form) {
|
||||
const mainStore = useMainStore();
|
||||
const token = btoa(form.username + ':' + form.password)
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
state: () => ({
|
||||
user: "",
|
||||
loginMethod: "",
|
||||
}),
|
||||
getters: {
|
||||
isAuthenticated: (state) => !!state.user,
|
||||
StateUser: (state) => state.user,
|
||||
},
|
||||
actions: {
|
||||
async LogIn(router, form) {
|
||||
const mainStore = useMainStore();
|
||||
const token = btoa(`${form.username}:${form.password}`);
|
||||
|
||||
api.defaults.headers.common['Authorization'] = `Basic ${token}`;
|
||||
try {
|
||||
await mainStore.loadServerConfigs(router);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
delete api.defaults.headers.common['Authorization'];
|
||||
throw new Error('Invalid username or password')
|
||||
}
|
||||
api.defaults.headers.common["Authorization"] = `Basic ${token}`;
|
||||
try {
|
||||
await mainStore.loadServerConfigs(router);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
delete api.defaults.headers.common["Authorization"];
|
||||
throw new Error("Invalid username or password");
|
||||
}
|
||||
|
||||
api.defaults.headers.common.Authorization = `Basic ${token}`
|
||||
api.defaults.headers.common.Authorization = `Basic ${token}`;
|
||||
|
||||
this.loginMethod = 'basic'
|
||||
this.user = form.email
|
||||
this.loginMethod = "basic";
|
||||
this.user = form.email;
|
||||
|
||||
if (form.remind === true) {
|
||||
localStorage.setItem(SESSION_KEY, token);
|
||||
} else {
|
||||
sessionStorage.setItem(SESSION_KEY, token);
|
||||
}
|
||||
if (form.remind === true) {
|
||||
localStorage.setItem(SESSION_KEY, token);
|
||||
} else {
|
||||
sessionStorage.setItem(SESSION_KEY, token);
|
||||
}
|
||||
|
||||
router.replace(router.currentRoute.value.query?.next || '/');
|
||||
},
|
||||
async CheckLoginInStorage(router) {
|
||||
let token = sessionStorage.getItem(SESSION_KEY);
|
||||
if (!token) {
|
||||
token = localStorage.getItem(SESSION_KEY);
|
||||
}
|
||||
router.replace(router.currentRoute.value.query?.next || "/");
|
||||
},
|
||||
async CheckLoginInStorage(router) {
|
||||
let token = sessionStorage.getItem(SESSION_KEY);
|
||||
if (!token) {
|
||||
token = localStorage.getItem(SESSION_KEY);
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mainStore = useMainStore();
|
||||
api.defaults.headers.common['Authorization'] = `Basic ${token}`;
|
||||
const mainStore = useMainStore();
|
||||
api.defaults.headers.common["Authorization"] = `Basic ${token}`;
|
||||
|
||||
try {
|
||||
await mainStore.loadServerConfigs(router);
|
||||
} catch (e) {
|
||||
// Auth token expired
|
||||
delete api.defaults.headers.common['Authorization'];
|
||||
await router.replace({ name: 'login', query: { next: router.currentRoute.fullPath } });
|
||||
return
|
||||
}
|
||||
try {
|
||||
await mainStore.loadServerConfigs(router);
|
||||
} catch (e) {
|
||||
// Auth token expired
|
||||
delete api.defaults.headers.common["Authorization"];
|
||||
await router.replace({
|
||||
name: "login",
|
||||
query: { next: router.currentRoute.fullPath },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.user = atob(token).split(':')[0];
|
||||
this.loginMethod = 'basic'
|
||||
},
|
||||
async LogOut(router) {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
this.user = atob(token).split(":")[0];
|
||||
this.loginMethod = "basic";
|
||||
},
|
||||
async LogOut(router) {
|
||||
localStorage.removeItem(SESSION_KEY);
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
|
||||
this.user = '';
|
||||
this.loginMethod = '';
|
||||
await router.replace({ name: 'login'});
|
||||
},
|
||||
}
|
||||
this.user = "";
|
||||
this.loginMethod = "";
|
||||
await router.replace({ name: "login" });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { store } from 'quasar/wrappers'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createPinia } from "pinia";
|
||||
import { store } from "quasar/wrappers";
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
@@ -11,10 +11,10 @@ import { createPinia } from 'pinia'
|
||||
*/
|
||||
|
||||
export default store((/* { ssrContext } */) => {
|
||||
const pinia = createPinia()
|
||||
const pinia = createPinia();
|
||||
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
|
||||
return pinia
|
||||
})
|
||||
return pinia;
|
||||
});
|
||||
|
||||
@@ -1,71 +1,68 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { api } from "boot/axios";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useMainStore = defineStore('main', {
|
||||
state: () => ({
|
||||
// Config
|
||||
apiReadonly: true,
|
||||
username: '',
|
||||
version: '',
|
||||
dashboardUrl: '',
|
||||
showHiddenFiles: false,
|
||||
export const useMainStore = defineStore("main", {
|
||||
state: () => ({
|
||||
// Config
|
||||
apiReadonly: true,
|
||||
username: "",
|
||||
version: "",
|
||||
dashboardUrl: "",
|
||||
showHiddenFiles: false,
|
||||
|
||||
// Frontend data
|
||||
buckets: [],
|
||||
}),
|
||||
getters: {
|
||||
serverUrl() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return process.env.VUE_APP_SERVER_URL || 'http://localhost:8787'
|
||||
}
|
||||
return window.location.origin
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async loadUserDisks() {
|
||||
const response = await api.get('/buckets')
|
||||
// Frontend data
|
||||
buckets: [],
|
||||
}),
|
||||
getters: {
|
||||
serverUrl() {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return process.env.VUE_APP_SERVER_URL || "http://localhost:8787";
|
||||
}
|
||||
return window.location.origin;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
async loadUserDisks() {
|
||||
const response = await api.get("/buckets");
|
||||
|
||||
this.buckets = response.data.buckets
|
||||
return response.data.buckets
|
||||
},
|
||||
async loadServerConfigs(router, handleError = false) {
|
||||
// This is the initial requests to server, that also checks if user needs auth
|
||||
this.buckets = response.data.buckets;
|
||||
return response.data.buckets;
|
||||
},
|
||||
async loadServerConfigs(router, handleError = false) {
|
||||
// This is the initial requests to server, that also checks if user needs auth
|
||||
|
||||
try {
|
||||
const response = await api.get('/server/config', {
|
||||
validateStatus: function (status) {
|
||||
return status >= 200 && status < 300
|
||||
}
|
||||
})
|
||||
try {
|
||||
const response = await api.get("/server/config", {
|
||||
validateStatus: (status) => status >= 200 && status < 300,
|
||||
});
|
||||
|
||||
this.apiReadonly = response.data.config.readonly;
|
||||
this.username = response.data.config.user?.username;
|
||||
this.version = response.data.config.version;
|
||||
this.dashboardUrl = response.data.config.dashboardUrl;
|
||||
this.showHiddenFiles = response.data.config.showHiddenFiles;
|
||||
this.apiReadonly = response.data.config.readonly;
|
||||
this.username = response.data.config.user?.username;
|
||||
this.version = response.data.config.version;
|
||||
this.dashboardUrl = response.data.config.dashboardUrl;
|
||||
this.showHiddenFiles = response.data.config.showHiddenFiles;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (error.response.status === 302) {
|
||||
// Handle cloudflare access login page
|
||||
const nextUrl = error.response.headers.Location;
|
||||
if (nextUrl) {
|
||||
window.location.replace(nextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
if (error.response.status === 302) {
|
||||
// Handle cloudflare access login page
|
||||
const nextUrl = error.response.headers.Location
|
||||
if (nextUrl) {
|
||||
window.location.replace(nextUrl)
|
||||
}
|
||||
}
|
||||
|
||||
if (handleError) {
|
||||
if (error.response?.status === 401) {
|
||||
await router.push({ name: 'login', query: { next: router.currentRoute.value.fullPath } })
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
throw error
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
if (handleError) {
|
||||
if (error.response?.status === 401) {
|
||||
await router.push({
|
||||
name: "login",
|
||||
query: { next: router.currentRoute.value.fullPath },
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
import "quasar/dist/types/feature-flag";
|
||||
|
||||
declare module "quasar/dist/types/feature-flag" {
|
||||
interface QuasarFeatureFlags {
|
||||
store: true;
|
||||
}
|
||||
interface QuasarFeatureFlags {
|
||||
store: true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "r2-explorer-github-action",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"publish": "wrangler publish"
|
||||
}
|
||||
"name": "r2-explorer-github-action",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"publish": "wrangler publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
// @ts-ignore
|
||||
const fs = require("fs");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const R2EXPLORER_WORKER_NAME = process.env.R2EXPLORER_WORKER_NAME;
|
||||
const WORKERS_CI = process.env.WORKERS_CI;
|
||||
let R2EXPLORER_WORKER_NAME = process.env.R2EXPLORER_WORKER_NAME;
|
||||
const R2EXPLORER_BUCKETS = process.env.R2EXPLORER_BUCKETS;
|
||||
const R2EXPLORER_CONFIG = process.env.R2EXPLORER_CONFIG;
|
||||
const R2EXPLORER_DOMAIN = process.env.R2EXPLORER_DOMAIN;
|
||||
const CF_API_TOKEN = process.env.CF_API_TOKEN;
|
||||
|
||||
let baseDir = __dirname;
|
||||
if (WORKERS_CI === "1") {
|
||||
baseDir = process.env.PWD;
|
||||
R2EXPLORER_WORKER_NAME = R2EXPLORER_WORKER_NAME || "r2-explorer";
|
||||
} else {
|
||||
if (!CF_API_TOKEN) {
|
||||
console.error("CF_API_TOKEN variable is required to continue!");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!R2EXPLORER_WORKER_NAME) {
|
||||
console.error("R2EXPLORER_WORKER_NAME variable is required to continue!");
|
||||
process.exit(1);
|
||||
console.error("R2EXPLORER_WORKER_NAME variable is required to continue!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!R2EXPLORER_BUCKETS) {
|
||||
console.error("R2EXPLORER_BUCKETS variable is required to continue!");
|
||||
process.exit(1);
|
||||
console.error("R2EXPLORER_BUCKETS variable is required to continue!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!R2EXPLORER_CONFIG) {
|
||||
console.error("R2EXPLORER_CONFIG variable is required to continue!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!CF_API_TOKEN) {
|
||||
console.error("CF_API_TOKEN variable is required to continue!");
|
||||
process.exit(1);
|
||||
console.error("R2EXPLORER_CONFIG variable is required to continue!");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let wranglerConfig = `
|
||||
@@ -34,49 +41,51 @@ main = "src/index.ts"
|
||||
`;
|
||||
|
||||
if (R2EXPLORER_DOMAIN) {
|
||||
wranglerConfig += `
|
||||
wranglerConfig += `
|
||||
workers_dev = false
|
||||
routes = [
|
||||
{ pattern = "${R2EXPLORER_DOMAIN}", custom_domain = true }
|
||||
]
|
||||
`;
|
||||
} else {
|
||||
wranglerConfig += `
|
||||
wranglerConfig += `
|
||||
workers_dev = true
|
||||
`;
|
||||
}
|
||||
|
||||
for (const bucket of R2EXPLORER_BUCKETS.split("\n")) {
|
||||
const split = bucket.trim().split(":");
|
||||
if (split.length !== 2) {
|
||||
console.error("R2EXPLORER_BUCKETS is not set correctly!");
|
||||
console.error(`"${split}" is not in the correct format`);
|
||||
process.exit(1);
|
||||
}
|
||||
const split = bucket.trim().split(":");
|
||||
if (split.length !== 2) {
|
||||
console.error("R2EXPLORER_BUCKETS is not set correctly!");
|
||||
console.error(`"${split}" is not in the correct format`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
wranglerConfig += `
|
||||
wranglerConfig += `
|
||||
[[r2_buckets]]
|
||||
binding = '${split[0]}'
|
||||
bucket_name = '${split[1]}'
|
||||
preview_bucket_name = '${split[1]}'
|
||||
`;
|
||||
|
||||
}
|
||||
|
||||
console.log(wranglerConfig)
|
||||
fs.writeFileSync(__dirname + "/wrangler.toml", wranglerConfig);
|
||||
console.log(wranglerConfig);
|
||||
fs.writeFileSync(`${baseDir}/wrangler.toml`, wranglerConfig);
|
||||
|
||||
if (!fs.existsSync(__dirname + "/src/")){
|
||||
fs.mkdirSync(__dirname + "/src/");
|
||||
if (!fs.existsSync(`${baseDir}/src/`)) {
|
||||
fs.mkdirSync(`${baseDir}/src/`);
|
||||
}
|
||||
|
||||
console.log(`
|
||||
import { R2Explorer } from "r2-explorer";
|
||||
|
||||
export default R2Explorer(${R2EXPLORER_CONFIG});
|
||||
`)
|
||||
fs.writeFileSync(__dirname + "/src/index.ts", `
|
||||
`);
|
||||
fs.writeFileSync(
|
||||
`${baseDir}/src/index.ts`,
|
||||
`
|
||||
import { R2Explorer } from "r2-explorer";
|
||||
|
||||
export default R2Explorer(${R2EXPLORER_CONFIG});
|
||||
`);
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
const fs = require('fs')
|
||||
const fs = require("node:fs");
|
||||
|
||||
// Apply version numbers
|
||||
const files = ['packages/worker/src/settings.ts', 'packages/worker/package.json']
|
||||
const files = [
|
||||
"packages/worker/src/settings.ts",
|
||||
"packages/worker/package.json",
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
fs.readFile(file, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
}
|
||||
fs.readFile(file, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = process.env.RELEASE_VERSION.replace('v', '')
|
||||
const result = data.replace('0.0.1', version)
|
||||
const version = process.env.RELEASE_VERSION.replace("v", "");
|
||||
const result = data.replace("0.0.1", version);
|
||||
|
||||
fs.writeFile(file, result, 'utf8', function (err) {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
})
|
||||
fs.writeFile(file, result, "utf8", (err) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { R2Explorer } from "../src";
|
||||
|
||||
const baseConfig = {
|
||||
readonly: false,
|
||||
cors: true,
|
||||
showHiddenFiles: true,
|
||||
dashboardUrl: "https://dev.r2-explorer-dashboard.pages.dev/",
|
||||
cacheAssets: false,
|
||||
readonly: false,
|
||||
cors: true,
|
||||
showHiddenFiles: true,
|
||||
dashboardUrl: "https://dev.r2-explorer-dashboard.pages.dev/",
|
||||
cacheAssets: false,
|
||||
};
|
||||
|
||||
export default {
|
||||
async email(event, env, context) {
|
||||
await R2Explorer(baseConfig).email(event, env, context);
|
||||
},
|
||||
async fetch(request, env, context) {
|
||||
return R2Explorer({
|
||||
...baseConfig,
|
||||
basicAuth: {
|
||||
username: env.BASIC_USERNAME,
|
||||
password: env.BASIC_PASSWORD
|
||||
}
|
||||
}).fetch(request, env, context);
|
||||
}
|
||||
async email(event, env, context) {
|
||||
await R2Explorer(baseConfig).email(event, env, context);
|
||||
},
|
||||
async fetch(request, env, context) {
|
||||
return R2Explorer({
|
||||
...baseConfig,
|
||||
basicAuth: {
|
||||
username: env.BASIC_USERNAME,
|
||||
password: env.BASIC_PASSWORD,
|
||||
},
|
||||
}).fetch(request, env, context);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"name": "r2-explorer-dev-worker",
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy"
|
||||
}
|
||||
"name": "r2-explorer-dev-worker",
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"chanfana": "https://prerelease-registry.devprod.cloudflare.dev/chanfana/runs/12189183779/npm-package-chanfana-208"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,62 @@
|
||||
{
|
||||
"name": "r2-explorer",
|
||||
"version": "0.0.1",
|
||||
"description": "A Google Drive Interface for your Cloudflare R2 Buckets",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "pinst --disable",
|
||||
"postpublish": "pinst --enable",
|
||||
"build": "node tools/cleanup umd && rollup --config rollup.config.js",
|
||||
"clean": "node tools/cleanup",
|
||||
"package": "npm run build && npm pack",
|
||||
"test": "jest --no-cache --runInBand",
|
||||
"test:cov": "jest --coverage --no-cache --runInBand",
|
||||
"addscope": "node tools/packagejson name @g4brym/r2-explorer",
|
||||
"prettify": "prettier . --write --ignore-unknown",
|
||||
"watch": "npm-watch build",
|
||||
"publish": "npm publish --access public"
|
||||
},
|
||||
"watch": {
|
||||
"build": {
|
||||
"patterns": [
|
||||
"src"
|
||||
],
|
||||
"extensions": "js,ts",
|
||||
"legacyWatch": true,
|
||||
"runOnChangeOnly": false
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"cloudflare",
|
||||
"worker",
|
||||
"workers",
|
||||
"serverless",
|
||||
"cloudflare r2",
|
||||
"r2",
|
||||
"r2 storage",
|
||||
"drive",
|
||||
"google drive",
|
||||
"ui",
|
||||
"cf",
|
||||
"typescript",
|
||||
"npm",
|
||||
"package",
|
||||
"cjs",
|
||||
"esm",
|
||||
"umd",
|
||||
"typed"
|
||||
],
|
||||
"author": "Gabriel Massadas",
|
||||
"license": "MIT",
|
||||
"homepage": "https://r2explorer.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "[email protected]:G4brym/R2-Explorer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/G4brym/R2-Explorer/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240222.0",
|
||||
"@rollup/plugin-terser": "^0.4.3",
|
||||
"@rollup/plugin-typescript": "^11.1.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/service-worker-mock": "^2.0.1",
|
||||
"npm-watch": "^0.11.0",
|
||||
"pinst": "^2.1.6",
|
||||
"rollup": "^2.36.1",
|
||||
"rollup-plugin-bundle-size": "^1.0.3",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"service-worker-mock": "^2.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4",
|
||||
"wrangler": "^3.32.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cloudflare/itty-router-openapi": "1.0.7",
|
||||
"itty-router": "4.0.26",
|
||||
"postal-mime": "^2.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"r2-explorer": "bin/r2-explorer.js"
|
||||
}
|
||||
"name": "r2-explorer",
|
||||
"version": "0.0.1",
|
||||
"description": "A Google Drive Interface for your Cloudflare R2 Buckets",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": ["dist", "LICENSE", "README.md"],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format cjs,esm --dts",
|
||||
"lint": "npx @biomejs/biome check src/ tests/ || (npx @biomejs/biome check --write src/ tests/; exit 1)",
|
||||
"test": "vitest run --root tests",
|
||||
"prepare": "husky",
|
||||
"package": "npm run build && npm pack"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"cloudflare",
|
||||
"worker",
|
||||
"workers",
|
||||
"serverless",
|
||||
"cloudflare r2",
|
||||
"r2",
|
||||
"r2 storage",
|
||||
"drive",
|
||||
"google drive",
|
||||
"ui",
|
||||
"cf",
|
||||
"typescript",
|
||||
"npm",
|
||||
"package",
|
||||
"cjs",
|
||||
"esm",
|
||||
"umd",
|
||||
"typed"
|
||||
],
|
||||
"author": "Gabriel Massadas",
|
||||
"license": "MIT",
|
||||
"homepage": "https://r2explorer.dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "[email protected]:G4brym/R2-Explorer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/G4brym/R2-Explorer/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20241127.0",
|
||||
"@types/node": "^22.10.1",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.2",
|
||||
"wrangler": "^3.91.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chanfana": "^2.4.2",
|
||||
"hono": "^4.6.12",
|
||||
"postal-mime": "^2.3.2",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import bundleSize from 'rollup-plugin-bundle-size'
|
||||
import { defineConfig } from 'rollup'
|
||||
import terser from '@rollup/plugin-terser'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
|
||||
export default defineConfig({
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{ format: 'cjs', file: 'dist/index.js' },
|
||||
{ format: 'es', file: 'dist/index.mjs' },
|
||||
],
|
||||
plugins: [
|
||||
typescript({
|
||||
sourceMap: false,
|
||||
filterRoot: 'src'
|
||||
}),
|
||||
terser(),
|
||||
bundleSize(),
|
||||
copy({
|
||||
targets: [
|
||||
{
|
||||
src: ['../LICENSE', '../README.md'],
|
||||
dest: '.',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
external: ['itty-router', 'zod', '@cloudflare/itty-router-openapi', 'postal-mime'],
|
||||
})
|
||||
@@ -1,170 +0,0 @@
|
||||
// This var will hold already imported jwt keys, this reduces the load of importing the key on every request
|
||||
import {Context, R2ExplorerConfig} from "../../interfaces";
|
||||
import {getCurrentTimestampSeconds} from "../../dates";
|
||||
|
||||
let builtJWTKeys: Record<string, CryptoKey> = {}
|
||||
let JWTExpiration = 0
|
||||
|
||||
function getAccessHost(teamName: string): string {
|
||||
return `https://${teamName}.cloudflareaccess.com`
|
||||
}
|
||||
|
||||
|
||||
export async function validateAccessJwt(request: any, env: any, context: Context) {
|
||||
let decodedJwt: any = false
|
||||
try {
|
||||
decodedJwt = await isValidJwt(request, context.config)
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (decodedJwt === false) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
errors: [{code: 10000, message: 'Authentication error! Verify that the Cloudflare Access Team name is correct'}]
|
||||
}, {status: 401})
|
||||
}
|
||||
|
||||
context.username = decodedJwt.payload.email
|
||||
}
|
||||
|
||||
async function getPublicKeys(config: R2ExplorerConfig): Promise<Record<string, CryptoKey>> {
|
||||
let jwtUrl = `${getAccessHost(config.cfAccessTeamName)}/cdn-cgi/access/certs`
|
||||
|
||||
const result = await fetch(jwtUrl, {
|
||||
method: 'GET',
|
||||
// @ts-ignore
|
||||
cf: {
|
||||
// Dont cache error responses
|
||||
cacheTtlByStatus: {'200-299': 30, '300-599': 0},
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
})
|
||||
|
||||
const data: any = await result.json()
|
||||
|
||||
JWTExpiration = getCurrentTimestampSeconds() + 3600 // 1h
|
||||
const importedKeys: Record<string, CryptoKey> = {}
|
||||
for (const key of data.keys) {
|
||||
importedKeys[key.kid] = await crypto.subtle.importKey(
|
||||
'jwk',
|
||||
key,
|
||||
{
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
false,
|
||||
['verify'],
|
||||
)
|
||||
}
|
||||
|
||||
return importedKeys
|
||||
}
|
||||
|
||||
export async function isValidJwt(request, config: R2ExplorerConfig) {
|
||||
// Get raw jwt token
|
||||
const encodedToken = getJwt(request)
|
||||
if (encodedToken === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Load jwt keys if they are not in memory or already expired
|
||||
if (Object.keys(builtJWTKeys).length === 0 || Math.floor(Date.now() / 1000) < JWTExpiration) {
|
||||
builtJWTKeys = await getPublicKeys(config)
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
let token
|
||||
try {
|
||||
token = decodeJwt(encodedToken)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Is the token expired?
|
||||
const expiryDate = new Date(token.payload.exp * 1000)
|
||||
const currentDate = new Date(Date.now())
|
||||
if (expiryDate <= currentDate) {
|
||||
console.log('expired token')
|
||||
return false
|
||||
}
|
||||
|
||||
if (token.payload?.iss !== getAccessHost(config.cfAccessTeamName)) {
|
||||
console.log('invalid access signer')
|
||||
return false
|
||||
}
|
||||
|
||||
// Check is token is valid against all public keys
|
||||
if (!isValidJwtSignature(token)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// All good, return payload
|
||||
return token
|
||||
}
|
||||
|
||||
/**
|
||||
* For this example, the JWT is passed in as part of the Authorization header,
|
||||
* after the Bearer scheme.
|
||||
* Parse the JWT out of the header and return it.
|
||||
*/
|
||||
function getJwt(request) {
|
||||
const authHeader = request.headers.get('cf-access-jwt-assertion')
|
||||
if (!authHeader) {
|
||||
return null
|
||||
}
|
||||
return authHeader.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and decode a JWT.
|
||||
* A JWT is three, base64 encoded, strings concatenated with ‘.’:
|
||||
* a header, a payload, and the signature.
|
||||
* The signature is “URL safe”, in that ‘/+’ characters have been replaced by ‘_-’
|
||||
*
|
||||
* Steps:
|
||||
* 1. Split the token at the ‘.’ character
|
||||
* 2. Base64 decode the individual parts
|
||||
* 3. Retain the raw Bas64 encoded strings to verify the signature
|
||||
*/
|
||||
function decodeJwt(token) {
|
||||
const parts = token.split('.')
|
||||
const header = JSON.parse(atob(parts[0]))
|
||||
const payload = JSON.parse(atob(parts[1]))
|
||||
const signature = atob(parts[2].replace(/_/g, '/').replace(/-/g, '+'))
|
||||
// console.log(header)
|
||||
return {
|
||||
header: header,
|
||||
payload: payload,
|
||||
signature: signature,
|
||||
raw: {header: parts[0], payload: parts[1], signature: parts[2]},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the JWT.
|
||||
*
|
||||
* Steps:
|
||||
* Reconstruct the signed message from the Base64 encoded strings.
|
||||
* Load the RSA public key into the crypto library.
|
||||
* Verify the signature with the message and the key.
|
||||
*/
|
||||
async function isValidJwtSignature(token) {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode([token.raw.header, token.raw.payload].join('.'))
|
||||
// @ts-ignore
|
||||
const signature = new Uint8Array(Array.from(token.signature).map((c) => c.charCodeAt(0)))
|
||||
|
||||
for (const key of Object.values(builtJWTKeys)) {
|
||||
const isValid = await validateSingleKey(key, signature, data)
|
||||
|
||||
if (isValid) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function validateSingleKey(key, signature, data): Promise<boolean> {
|
||||
return crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Context } from "../../interfaces";
|
||||
|
||||
|
||||
export async function validateBasicAuth(request: any, env: any, context: Context) {
|
||||
let decodedCredentials: any = false;
|
||||
let username = undefined
|
||||
|
||||
try {
|
||||
decodedCredentials = getBasicAuth(request).split(':')
|
||||
|
||||
if (!Array.isArray(context.config.basicAuth)) {
|
||||
context.config.basicAuth = [context.config.basicAuth]
|
||||
}
|
||||
|
||||
for (const credentials of context.config.basicAuth) {
|
||||
if (credentials.username === decodedCredentials[0] && credentials.password === decodedCredentials[1]) {
|
||||
username = credentials.username
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (decodedCredentials === false || username === undefined) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
errors: [{ code: 10000, message: "Authentication error! This server requires Basic Auth" }]
|
||||
}, { status: 401 });
|
||||
}
|
||||
|
||||
context.username = username;
|
||||
}
|
||||
|
||||
function getBasicAuth(request) {
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
if (!authHeader) {
|
||||
return null;
|
||||
}
|
||||
return atob(authHeader.replace('Basic', '').trim());
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import {OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from "zod";
|
||||
|
||||
export class CreateFolder extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-bucket-create-folder',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Create folder',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
},
|
||||
requestBody: {
|
||||
key: z.string().describe('base64 encoded file key'),
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
const key = decodeURIComponent(escape(atob(data.body.key)))
|
||||
|
||||
return await bucket.put(key, 'R2 Explorer Folder')
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import {OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from "zod";
|
||||
|
||||
export class DeleteObject extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-bucket-delete-object',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Delete object',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
},
|
||||
requestBody: {
|
||||
key: z.string().describe('base64 encoded file key'),
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
const key = decodeURIComponent(escape(atob(data.body.key)))
|
||||
|
||||
await bucket.delete(key)
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import {OpenAPIRoute, Path} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from 'zod'
|
||||
|
||||
export class GetObject extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'get-bucket-object',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Get Object',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
key: Path(z.string().describe('base64 encoded file key')),
|
||||
},
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'File binary',
|
||||
schema: z.string().openapi({format: 'binary'}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
let filePath
|
||||
try {
|
||||
filePath = decodeURIComponent(escape(atob(data.params.key)));
|
||||
} catch (e) {
|
||||
filePath = decodeURIComponent(escape(atob(decodeURIComponent(data.params.key))));
|
||||
}
|
||||
|
||||
const object = await bucket.get(filePath)
|
||||
|
||||
if (object === null) {
|
||||
return Response.json({msg: 'Object Not Found'}, {status: 404})
|
||||
}
|
||||
|
||||
const headers = new Headers()
|
||||
object.writeHttpMetadata(headers)
|
||||
headers.set('etag', object.httpEtag)
|
||||
headers.set('Content-Disposition', `attachment; filename="${filePath.split('/').pop()}"`)
|
||||
|
||||
return new Response(object.body, {
|
||||
headers,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import {OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from 'zod'
|
||||
|
||||
export class HeadObject extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'Head-bucket-object',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Get Object',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
key: Path(z.string().describe('base64 encoded file key')),
|
||||
},
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
let filePath
|
||||
try {
|
||||
filePath = decodeURIComponent(escape(atob(data.params.key)));
|
||||
} catch (e) {
|
||||
filePath = decodeURIComponent(escape(atob(decodeURIComponent(data.params.key))));
|
||||
}
|
||||
|
||||
const object = await bucket.head(filePath)
|
||||
|
||||
if (object === null) {
|
||||
return Response.json({msg: 'Object Not Found'}, {status: 404})
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import {OpenAPIRoute, Path} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
|
||||
export class ListBuckets extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'get-bucket-list',
|
||||
tags: ['Buckets'],
|
||||
summary: 'List buckets',
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
const buckets = []
|
||||
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
// @ts-ignore - check if the field in Env is actually a R2 bucket by its properties
|
||||
if (value.get && value.put) {
|
||||
buckets.push({name: key})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buckets: buckets,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { OpenAPIRoute, Path, Query } from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from 'zod'
|
||||
|
||||
export class ListObjects extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'get-bucket-list-objects',
|
||||
tags: ['Buckets'],
|
||||
summary: 'List objects',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
limit: Query(Number, {required: false}),
|
||||
prefix: Query(z.string().nullable().optional().describe('base64 encoded prefix'),),
|
||||
cursor: Query(z.string().nullable().optional()),
|
||||
delimiter: Query(z.string().nullable().optional()),
|
||||
include: Query(z.string().array().optional()),
|
||||
},
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
return await bucket.list({
|
||||
limit: data.query.limit,
|
||||
prefix: data.query.prefix ? decodeURIComponent(escape(atob(data.query.prefix))) : undefined,
|
||||
cursor: data.query.cursor,
|
||||
delimiter: (data.query.delimiter) ? data.query.delimiter : '',
|
||||
include: data.query.include,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import {OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from "zod";
|
||||
|
||||
export class MoveObject extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-bucket-move-object',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Move object',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
},
|
||||
requestBody: {
|
||||
oldKey: z.string().describe('base64 encoded file key'),
|
||||
newKey: z.string().describe('base64 encoded file key'),
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
const oldKey = decodeURIComponent(escape(atob(data.body.oldKey)))
|
||||
const newKey = decodeURIComponent(escape(atob(data.body.newKey)))
|
||||
|
||||
const object = await bucket.get(oldKey)
|
||||
const resp = await bucket.put(newKey, object.body, {customMetadata: object.customMetadata, httpMetadata: object.httpMetadata})
|
||||
|
||||
await bucket.delete(oldKey)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import {Int, OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from "zod";
|
||||
|
||||
export class CompleteUpload extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-multipart-complete-upload',
|
||||
tags: ['Multipart'],
|
||||
summary: 'Complete upload',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
},
|
||||
requestBody: {
|
||||
uploadId: String,
|
||||
key: z.string().describe('base64 encoded file key'),
|
||||
parts: [{
|
||||
etag: String,
|
||||
partNumber: Int
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
const uploadId = data.body.uploadId
|
||||
const key = decodeURIComponent(escape(atob(data.body.key)))
|
||||
const parts = data.body.parts
|
||||
|
||||
const multipartUpload = await bucket.resumeMultipartUpload(key, uploadId);
|
||||
|
||||
try {
|
||||
const resp = await multipartUpload.complete(parts);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
str: resp,
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
return Response.json({msg: error.message}, {status: 400});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import {OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from "zod";
|
||||
|
||||
export class CreateUpload extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-multipart-create-upload',
|
||||
tags: ['Multipart'],
|
||||
summary: 'Create upload',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
key: Query(z.string().describe('base64 encoded file key')),
|
||||
customMetadata: Query(z.string().nullable().optional().describe('base64 encoded json string')),
|
||||
httpMetadata: Query(z.string().nullable().optional().describe('base64 encoded json string')),
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
const key = decodeURIComponent(escape(atob(data.query.key)))
|
||||
|
||||
let customMetadata = undefined
|
||||
if (data.query.customMetadata) {
|
||||
customMetadata = JSON.parse(decodeURIComponent(escape(atob(data.query.customMetadata))))
|
||||
}
|
||||
|
||||
let httpMetadata = undefined
|
||||
if (data.query.httpMetadata) {
|
||||
httpMetadata = JSON.parse(decodeURIComponent(escape(atob(data.query.httpMetadata))))
|
||||
}
|
||||
|
||||
return await bucket.createMultipartUpload(key, {customMetadata: customMetadata, httpMetadata: httpMetadata});
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Int, OpenAPIRoute, Path, Query, RequestBody } from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from "zod";
|
||||
|
||||
export class PartUpload extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-multipart-part-upload',
|
||||
tags: ['Multipart'],
|
||||
summary: 'Part upload',
|
||||
requestBody: new RequestBody({
|
||||
content: {
|
||||
'application/octet-stream': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
key: Query(z.string().describe('base64 encoded file key')),
|
||||
uploadId: Query(String),
|
||||
partNumber: Query(Int),
|
||||
}
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
const key = decodeURIComponent(escape(atob(data.query.key)))
|
||||
|
||||
const multipartUpload = bucket.resumeMultipartUpload(key, data.query.uploadId);
|
||||
|
||||
try {
|
||||
return await multipartUpload.uploadPart(data.query.partNumber, request.body);
|
||||
|
||||
} catch (error: any) {
|
||||
return new Response(error.message, {status: 400});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import {OpenAPIRoute, Path, Query} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from 'zod'
|
||||
|
||||
export class PutMetadata extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-bucket-put-object-metadata',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Update object metadata',
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
key: Path(z.string().describe('base64 encoded file key')),
|
||||
},
|
||||
requestBody: z.object({
|
||||
customMetadata: z.record(z.string(), z.any())
|
||||
}).openapi("Object metadata")
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
let filePath
|
||||
try {
|
||||
filePath = decodeURIComponent(escape(atob(data.params.key)));
|
||||
} catch (e) {
|
||||
filePath = decodeURIComponent(escape(atob(decodeURIComponent(data.params.key))));
|
||||
}
|
||||
|
||||
const object = await bucket.get(filePath)
|
||||
return await bucket.put(filePath, object.body, {customMetadata: data.body.customMetadata})
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { OpenAPIRoute, Path, Query, RequestBody } from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {z} from 'zod'
|
||||
|
||||
export class PutObject extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'post-bucket-upload-object',
|
||||
tags: ['Buckets'],
|
||||
summary: 'Upload object',
|
||||
requestBody: new RequestBody({
|
||||
content: {
|
||||
'application/octet-stream': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
parameters: {
|
||||
bucket: Path(String),
|
||||
key: Query(z.string().describe('base64 encoded file key')),
|
||||
customMetadata: Query(z.string().nullable().optional().describe('base64 encoded json string')),
|
||||
httpMetadata: Query(z.string().nullable().optional().describe('base64 encoded json string')),
|
||||
},
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({msg: 'unauthorized'}, {status: 401})
|
||||
|
||||
const bucket = env[data.params.bucket]
|
||||
|
||||
let key = decodeURIComponent(escape(atob(data.query.key)))
|
||||
|
||||
let customMetadata = undefined
|
||||
if (data.query.customMetadata) {
|
||||
customMetadata = JSON.parse(decodeURIComponent(escape(atob(data.query.customMetadata))))
|
||||
}
|
||||
|
||||
let httpMetadata = undefined
|
||||
if (data.query.httpMetadata) {
|
||||
httpMetadata = JSON.parse(decodeURIComponent(escape(atob(data.query.httpMetadata))))
|
||||
}
|
||||
|
||||
return await bucket.put(key, request.body, {customMetadata: customMetadata, httpMetadata: httpMetadata})
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import {config} from "../settings";
|
||||
import {ListBuckets} from "./api/listBuckets";
|
||||
import {ListObjects} from "./api/listObjects";
|
||||
import {MoveObject} from "./api/moveObject";
|
||||
import {CreateFolder} from "./api/createFolder";
|
||||
import {PutObject} from "./api/putObject";
|
||||
import {DeleteObject} from "./api/deleteObject";
|
||||
import {CreateUpload} from "./api/multipart/createUpload";
|
||||
import {PartUpload} from "./api/multipart/partUpload";
|
||||
import {CompleteUpload} from "./api/multipart/completeUpload";
|
||||
import {OpenAPIRouter} from "@cloudflare/itty-router-openapi";
|
||||
import {GetObject} from "./api/getObject";
|
||||
import {HeadObject} from "./api/headObject";
|
||||
import {PutMetadata} from "./api/putMetadata";
|
||||
|
||||
|
||||
export const bucketsRouter = OpenAPIRouter({
|
||||
base: '/api/buckets',
|
||||
raiseUnknownParameters: config.raiseUnknownParameters,
|
||||
generateOperationIds: config.generateOperationIds,
|
||||
})
|
||||
|
||||
bucketsRouter.get('', ListBuckets)
|
||||
bucketsRouter.get('/:bucket', ListObjects)
|
||||
bucketsRouter.post('/:bucket/move', MoveObject)
|
||||
bucketsRouter.post('/:bucket/folder', CreateFolder)
|
||||
bucketsRouter.post('/:bucket/upload', PutObject)
|
||||
bucketsRouter.post('/:bucket/multipart/create', CreateUpload)
|
||||
bucketsRouter.post('/:bucket/multipart/upload', PartUpload)
|
||||
bucketsRouter.post('/:bucket/multipart/complete', CompleteUpload)
|
||||
bucketsRouter.post('/:bucket/delete', DeleteObject)
|
||||
bucketsRouter.head('/:bucket/:key', HeadObject)
|
||||
bucketsRouter.get('/:bucket/:key/head', HeadObject) // There are some issues with calling the head method
|
||||
bucketsRouter.get('/:bucket/:key', GetObject)
|
||||
bucketsRouter.post('/:bucket/:key', PutMetadata)
|
||||
@@ -1,56 +0,0 @@
|
||||
import {Context} from "./interfaces";
|
||||
|
||||
export async function dashboardProxy(request: any, env: any, context: Context) {
|
||||
// Initialize the default cache
|
||||
//@ts-ignore
|
||||
const cache = caches.default
|
||||
|
||||
let path = new URL(request.url).pathname
|
||||
|
||||
// Support page navigation
|
||||
if (!path.includes('.')) {
|
||||
path = "/"
|
||||
}
|
||||
|
||||
let result
|
||||
|
||||
if (context.config.cacheAssets !== false) {
|
||||
// use .match() to see if we have a cache hit, if so return the caches response early
|
||||
result = await cache.match(request)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
let dashboardUrl = 'https://demo.r2explorer.dev'
|
||||
if (context.config?.dashboardUrl) {
|
||||
if (context.config.dashboardUrl.endsWith('/')) {
|
||||
dashboardUrl = context.config.dashboardUrl.slice(0, -1)
|
||||
} else {
|
||||
dashboardUrl = context.config.dashboardUrl
|
||||
}
|
||||
}
|
||||
|
||||
// we'll chain our await calls to get the JSON response in one line
|
||||
const response = await fetch(
|
||||
`${dashboardUrl}${path}`
|
||||
)
|
||||
|
||||
result = new Response(await response.body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': response.headers.get('Content-Type'),
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
// We set a max-age of 300 seconds which is equivalent to 5 minutes.
|
||||
// If the last response is older than that the cache.match() call returns nothing and and a new response is fetched
|
||||
'Cache-Control': 'max-age: 300',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 200 && context.config.cacheAssets !== false) {
|
||||
// before returning the response we put a clone of our response object into the cache so it can be resolved later
|
||||
context.executionContext.waitUntil(cache.put(request, result.clone()))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { OpenAPIRoute } from "@cloudflare/itty-router-openapi";
|
||||
import { Context } from "../../interfaces";
|
||||
import { OpenAPIRouteSchema } from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
|
||||
export class SendEmail extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: "post-email-send",
|
||||
tags: ["Emails"],
|
||||
summary: "Send Email",
|
||||
requestBody: {
|
||||
subject: "Look! No servers",
|
||||
from: {
|
||||
email: "[email protected]",
|
||||
name: "Workers - MailChannels integration"
|
||||
},
|
||||
to: [{ email: "[email protected]", name: "Test Recipient" }],
|
||||
content: {
|
||||
"text/plain": "And no email service accounts and all for free too!"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
if (context.config.readonly === true) return Response.json({ msg: "unauthorized" }, { status: 401 });
|
||||
|
||||
const emailResp = await fetch("https://api.mailchannels.net/tx/v1/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
personalizations: [
|
||||
{
|
||||
to: data.body.to
|
||||
}
|
||||
],
|
||||
from: data.body.from,
|
||||
subject: data.body.subject,
|
||||
content: [
|
||||
{
|
||||
type: "text/plain",
|
||||
value: data.body.content["text/plain"]
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
return { msg: await emailResp.text() };
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import {getCurrentTimestampMilliseconds} from "../dates";
|
||||
import {Context} from "../interfaces";
|
||||
import PostalMime from 'postal-mime';
|
||||
|
||||
async function streamToArrayBuffer(stream, streamSize) {
|
||||
let result = new Uint8Array(streamSize);
|
||||
let bytesRead = 0;
|
||||
const reader = stream.getReader();
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
result.set(value, bytesRead);
|
||||
bytesRead += value.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function receiveEmail(event, env, ctx: Context) {
|
||||
let bucket = undefined
|
||||
|
||||
if (ctx.config?.emailRouting?.targetBucket && env[ctx.config.emailRouting.targetBucket]) {
|
||||
bucket = env[ctx.config.emailRouting.targetBucket]
|
||||
}
|
||||
|
||||
if (!bucket) {
|
||||
// Bucket not set, default to first defined
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
// @ts-ignore
|
||||
if (value.get && value.put) {
|
||||
bucket = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rawEmail = await streamToArrayBuffer(event.raw, event.rawSize);
|
||||
const parser = new PostalMime()
|
||||
const parsedEmail = await parser.parse(rawEmail);
|
||||
|
||||
const emailPath = `${getCurrentTimestampMilliseconds()}-${crypto.randomUUID()}`
|
||||
|
||||
await bucket.put(`.r2-explorer/emails/inbox/${emailPath}.json`, JSON.stringify(parsedEmail), {
|
||||
customMetadata: {
|
||||
subject: parsedEmail.subject,
|
||||
from_address: parsedEmail.from?.address,
|
||||
from_name: parsedEmail.from?.name,
|
||||
to_address: (parsedEmail.to.length > 0) ? parsedEmail.to[0].address : null,
|
||||
to_name: (parsedEmail.to.length > 0) ? parsedEmail.to[0].name : null,
|
||||
has_attachments: parsedEmail.attachments.length > 0,
|
||||
read: false,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
for (const att of parsedEmail.attachments) {
|
||||
await bucket.put(`.r2-explorer/emails/inbox/${emailPath}/${att.filename}`, att.content)
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { config } from "../settings";
|
||||
import { OpenAPIRouter } from "@cloudflare/itty-router-openapi";
|
||||
import { SendEmail } from "./api/sendEmail";
|
||||
|
||||
|
||||
export const emailsRouter = OpenAPIRouter({
|
||||
base: "/api/emails",
|
||||
raiseUnknownParameters: config.raiseUnknownParameters,
|
||||
generateOperationIds: config.generateOperationIds
|
||||
});
|
||||
|
||||
emailsRouter.post("/send", SendEmail);
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { AppContext } from "../types";
|
||||
|
||||
export async function dashboardProxy(c: AppContext) {
|
||||
// Initialize the default cache
|
||||
//@ts-ignore
|
||||
const cache = caches.default;
|
||||
|
||||
let path = new URL(c.req.raw.url).pathname;
|
||||
|
||||
// Support page navigation
|
||||
if (!path.includes(".")) {
|
||||
path = "/";
|
||||
}
|
||||
|
||||
let result;
|
||||
const config = c.get("config");
|
||||
|
||||
if (config.cacheAssets !== false) {
|
||||
// use .match() to see if we have a cache hit, if so return the caches response early
|
||||
result = await cache.match(c.req.raw);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
let dashboardUrl = "https://demo.r2explorer.dev";
|
||||
if (config.dashboardUrl) {
|
||||
if (config.dashboardUrl.endsWith("/")) {
|
||||
dashboardUrl = config.dashboardUrl.slice(0, -1);
|
||||
} else {
|
||||
dashboardUrl = config.dashboardUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// we'll chain our await calls to get the JSON response in one line
|
||||
const response = await fetch(`${dashboardUrl}${path}`);
|
||||
|
||||
result = new Response(await response.body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"Content-Type": response.headers.get("Content-Type"),
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
// We set a max-age of 300 seconds which is equivalent to 5 minutes.
|
||||
// If the last response is older than that the cache.match() call returns nothing and and a new response is fetched
|
||||
"Cache-Control": "max-age: 300",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status === 200 && config.cacheAssets !== false) {
|
||||
// before returning the response we put a clone of our response object into the cache so it can be resolved later
|
||||
c.executionCtx.waitUntil(cache.put(c.req.raw, result.clone()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export function getCurrentTimestampSeconds(): number {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
export function getCurrentTimestampMilliseconds(): number {
|
||||
return Math.floor(Date.now())
|
||||
return Math.floor(Date.now());
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import type { AppContext } from "../../types";
|
||||
import { getCurrentTimestampSeconds } from "../dates";
|
||||
|
||||
// This var will hold already imported jwt keys, this reduces the load of importing the key on every request
|
||||
let builtJWTKeys: Record<string, CryptoKey> = {};
|
||||
let JWTExpiration = 0;
|
||||
|
||||
function getAccessHost(teamName: string): string {
|
||||
return `https://${teamName}.cloudflareaccess.com`;
|
||||
}
|
||||
|
||||
export async function accessMiddleware(c: AppContext, next: CallableFunction) {
|
||||
const encodedToken = getJwt(c);
|
||||
|
||||
if (encodedToken === null) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10000,
|
||||
message: "Authentication error: Missing bearer token",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
const cfAccessTeamName = c.get("config").cfAccessTeamName as string;
|
||||
let decodedJwt: any = false;
|
||||
try {
|
||||
decodedJwt = await isValidJwt(encodedToken, cfAccessTeamName);
|
||||
} catch (e) {}
|
||||
|
||||
if (decodedJwt === false) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10001,
|
||||
message: "Authentication error: Unable to decode Bearer token",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
c.set("username", decodedJwt.payload.email);
|
||||
await next();
|
||||
}
|
||||
|
||||
async function getPublicKeys(
|
||||
cfAccessTeamName: string,
|
||||
): Promise<Record<string, CryptoKey>> {
|
||||
const jwtUrl = `${getAccessHost(cfAccessTeamName)}/cdn-cgi/access/certs`;
|
||||
|
||||
const result = await fetch(jwtUrl, {
|
||||
method: "GET",
|
||||
// @ts-ignore
|
||||
cf: {
|
||||
// Dont cache error responses
|
||||
cacheTtlByStatus: { "200-299": 30, "300-599": 0 },
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=UTF-8",
|
||||
},
|
||||
});
|
||||
|
||||
const data: any = await result.json();
|
||||
|
||||
JWTExpiration = getCurrentTimestampSeconds() + 3600; // 1h
|
||||
const importedKeys: Record<string, CryptoKey> = {};
|
||||
for (const key of data.keys) {
|
||||
importedKeys[key.kid] = await crypto.subtle.importKey(
|
||||
"jwk",
|
||||
key,
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
}
|
||||
|
||||
return importedKeys;
|
||||
}
|
||||
|
||||
export async function isValidJwt(
|
||||
encodedToken: string,
|
||||
cfAccessTeamName: string,
|
||||
) {
|
||||
// Load jwt keys if they are not in memory or already expired
|
||||
if (
|
||||
Object.keys(builtJWTKeys).length === 0 ||
|
||||
Math.floor(Date.now() / 1000) < JWTExpiration
|
||||
) {
|
||||
builtJWTKeys = await getPublicKeys(cfAccessTeamName);
|
||||
}
|
||||
|
||||
// Decode payload
|
||||
let token;
|
||||
try {
|
||||
token = decodeJwt(encodedToken);
|
||||
} catch (err) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10001,
|
||||
message: "Authentication error: Unable to decode Bearer token",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// Is the token expired?
|
||||
const expiryDate = new Date(token.payload.exp * 1000);
|
||||
const currentDate = new Date(Date.now());
|
||||
if (expiryDate <= currentDate) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10002,
|
||||
message: "Authentication error: Token is expired",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
if (token.payload?.iss !== getAccessHost(cfAccessTeamName)) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10003,
|
||||
message: `Authentication error: Expected team name ${cfAccessTeamName}, but received ${token.payload?.iss}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// Check is token is valid against all public keys
|
||||
if (!(await isValidJwtSignature(token))) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10004,
|
||||
message: "Authentication error: Invalid Token",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
// All good, return payload
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* For this example, the JWT is passed in as part of the Authorization header,
|
||||
* after the Bearer scheme.
|
||||
* Parse the JWT out of the header and return it.
|
||||
*/
|
||||
function getJwt(c: AppContext) {
|
||||
const authHeader = c.req.header("cf-access-jwt-assertion");
|
||||
if (!authHeader) {
|
||||
return null;
|
||||
}
|
||||
return authHeader.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and decode a JWT.
|
||||
* A JWT is three, base64 encoded, strings concatenated with ‘.’:
|
||||
* a header, a payload, and the signature.
|
||||
* The signature is “URL safe”, in that ‘/+’ characters have been replaced by ‘_-’
|
||||
*
|
||||
* Steps:
|
||||
* 1. Split the token at the ‘.’ character
|
||||
* 2. Base64 decode the individual parts
|
||||
* 3. Retain the raw Bas64 encoded strings to verify the signature
|
||||
*/
|
||||
type DecodedToken = {
|
||||
header: object;
|
||||
payload: { iss?: string; exp: number };
|
||||
signature: string;
|
||||
raw: { header?: string; payload?: string; signature?: string };
|
||||
};
|
||||
|
||||
function decodeJwt(token: string): DecodedToken {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
|
||||
const header = JSON.parse(atob(parts[0] as string));
|
||||
const payload = JSON.parse(atob(parts[1] as string));
|
||||
const signature = atob(
|
||||
(parts[2] as string).replace(/_/g, "/").replace(/-/g, "+"),
|
||||
);
|
||||
|
||||
return {
|
||||
header: header,
|
||||
payload: payload,
|
||||
signature: signature,
|
||||
raw: { header: parts[0], payload: parts[1], signature: parts[2] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the JWT.
|
||||
*
|
||||
* Steps:
|
||||
* Reconstruct the signed message from the Base64 encoded strings.
|
||||
* Load the RSA public key into the crypto library.
|
||||
* Verify the signature with the message and the key.
|
||||
*/
|
||||
async function isValidJwtSignature(token: DecodedToken) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode([token.raw.header, token.raw.payload].join("."));
|
||||
// @ts-ignore
|
||||
const signature = new Uint8Array(
|
||||
Array.from(token.signature).map((c) => c.charCodeAt(0)),
|
||||
);
|
||||
|
||||
for (const key of Object.values(builtJWTKeys)) {
|
||||
const isValid = await validateSingleKey(key, signature, data);
|
||||
|
||||
if (isValid) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function validateSingleKey(
|
||||
key: CryptoKey,
|
||||
signature: Uint8Array,
|
||||
data: Uint8Array,
|
||||
): Promise<boolean> {
|
||||
return crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, data);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export async function readOnlyMiddleware(
|
||||
c: AppContext,
|
||||
next: CallableFunction,
|
||||
) {
|
||||
const config = c.get("config");
|
||||
|
||||
if (config.readonly === true && !["GET", "HEAD"].includes(c.req.method)) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [
|
||||
{
|
||||
code: 10005,
|
||||
message:
|
||||
"This instance is in ReadOnly Mode, no changes are allowed!",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const settings = {
|
||||
version: "0.0.1",
|
||||
};
|
||||
+138
-72
@@ -1,87 +1,153 @@
|
||||
import { createCors } from "itty-router";
|
||||
import { R2ExplorerConfig } from "./interfaces";
|
||||
import { OpenAPIRouter } from "@cloudflare/itty-router-openapi";
|
||||
import { bucketsRouter } from "./buckets/router";
|
||||
import { validateAccessJwt } from "./authentication/api/access";
|
||||
import { dashboardProxy } from "./dashbord";
|
||||
import { receiveEmail } from "./emails/receiveEmail";
|
||||
import { serverRouter } from "./server/router";
|
||||
import { config as settings } from "./settings";
|
||||
import { validateBasicAuth } from "./authentication/api/basic";
|
||||
import { emailsRouter } from "./emails/router";
|
||||
import {
|
||||
type OpenAPIObjectConfigV31,
|
||||
extendZodWithOpenApi,
|
||||
fromHono,
|
||||
} from "chanfana";
|
||||
import { type ExecutionContext, Hono } from "hono";
|
||||
import { basicAuth } from "hono/basic-auth";
|
||||
import { cors } from "hono/cors";
|
||||
import { z } from "zod";
|
||||
import { dashboardProxy } from "./foundation/dashbord";
|
||||
import { accessMiddleware } from "./foundation/middlewares/authentication";
|
||||
import { readOnlyMiddleware } from "./foundation/middlewares/readonly";
|
||||
import { settings } from "./foundation/settings";
|
||||
import { CreateFolder } from "./modules/buckets/createFolder";
|
||||
import { DeleteObject } from "./modules/buckets/deleteObject";
|
||||
import { GetObject } from "./modules/buckets/getObject";
|
||||
import { HeadObject } from "./modules/buckets/headObject";
|
||||
import { ListBuckets } from "./modules/buckets/listBuckets";
|
||||
import { ListObjects } from "./modules/buckets/listObjects";
|
||||
import { MoveObject } from "./modules/buckets/moveObject";
|
||||
import { CompleteUpload } from "./modules/buckets/multipart/completeUpload";
|
||||
import { CreateUpload } from "./modules/buckets/multipart/createUpload";
|
||||
import { PartUpload } from "./modules/buckets/multipart/partUpload";
|
||||
import { PutMetadata } from "./modules/buckets/putMetadata";
|
||||
import { PutObject } from "./modules/buckets/putObject";
|
||||
import { receiveEmail } from "./modules/emails/receiveEmail";
|
||||
import { SendEmail } from "./modules/emails/sendEmail";
|
||||
import { GetInfo } from "./modules/server/getInfo";
|
||||
import type {
|
||||
AppContext,
|
||||
AppEnv,
|
||||
AppVariables,
|
||||
BasicAuth,
|
||||
R2ExplorerConfig,
|
||||
} from "./types";
|
||||
|
||||
export function R2Explorer(config?: R2ExplorerConfig) {
|
||||
config = config || {};
|
||||
if (config.readonly !== false) config.readonly = true;
|
||||
extendZodWithOpenApi(z);
|
||||
config = config || {};
|
||||
if (config.readonly !== false) config.readonly = true;
|
||||
|
||||
const openapiSchema = {
|
||||
info: {
|
||||
title: "R2 Explorer API",
|
||||
version: settings.version
|
||||
}
|
||||
};
|
||||
const openapiSchema: OpenAPIObjectConfigV31 = {
|
||||
openapi: "3.1.0",
|
||||
info: {
|
||||
title: "R2 Explorer API",
|
||||
version: settings.version,
|
||||
},
|
||||
};
|
||||
|
||||
if (config.basicAuth) {
|
||||
openapiSchema["security"] = [
|
||||
{
|
||||
basicAuth: []
|
||||
}
|
||||
];
|
||||
}
|
||||
if (config.basicAuth) {
|
||||
openapiSchema["security"] = [
|
||||
{
|
||||
basicAuth: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const router = OpenAPIRouter({
|
||||
schema: openapiSchema
|
||||
});
|
||||
const { preflight, corsify } = createCors({
|
||||
methods: ['*']
|
||||
});
|
||||
const app = new Hono<{ Bindings: AppEnv; Variables: AppVariables }>();
|
||||
app.use("*", async (c, next) => {
|
||||
c.set("config", config);
|
||||
await next();
|
||||
});
|
||||
|
||||
if (config.cors === true) {
|
||||
router.all("/api*", preflight);
|
||||
}
|
||||
const openapi = fromHono(app, {
|
||||
schema: openapiSchema,
|
||||
raiseUnknownParameters: true,
|
||||
generateOperationIds: false,
|
||||
});
|
||||
|
||||
// Check Access JWT first
|
||||
if (config.cfAccessTeamName) {
|
||||
router.all("*", validateAccessJwt);
|
||||
}
|
||||
if (config.cors === true) {
|
||||
app.use(
|
||||
"*",
|
||||
cors({
|
||||
origin: "*",
|
||||
allowMethods: ["*"],
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Check basic auth
|
||||
if (config.basicAuth) {
|
||||
router.registry.registerComponent(
|
||||
"securitySchemes",
|
||||
"basicAuth",
|
||||
{
|
||||
type: "http",
|
||||
scheme: "basic"
|
||||
}
|
||||
);
|
||||
router.all("/api*", validateBasicAuth);
|
||||
}
|
||||
if (config.readonly === true) {
|
||||
app.use(readOnlyMiddleware);
|
||||
}
|
||||
|
||||
router.all("/api/server/*", serverRouter);
|
||||
router.all("/api/buckets/*", bucketsRouter);
|
||||
router.all("/api/emails/*", emailsRouter);
|
||||
if (config.cfAccessTeamName) {
|
||||
app.use(accessMiddleware);
|
||||
}
|
||||
|
||||
router.original.get("*", dashboardProxy);
|
||||
if (config.basicAuth) {
|
||||
openapi.registry.registerComponent("securitySchemes", "basicAuth", {
|
||||
type: "http",
|
||||
scheme: "basic",
|
||||
});
|
||||
app.use(
|
||||
basicAuth({
|
||||
verifyUser: (username, password, c: AppContext) => {
|
||||
const users = (
|
||||
Array.isArray(c.get("config").basicAuth)
|
||||
? c.get("config").basicAuth
|
||||
: [c.get("config").basicAuth]
|
||||
) as BasicAuth[];
|
||||
|
||||
router.all("*", () => Response.json({ msg: "404, not found!" }, { status: 404 }));
|
||||
for (const user of users) {
|
||||
if (user.username === username && user.password === password) {
|
||||
c.set("username", username);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async email(event, env, context) {
|
||||
await receiveEmail(event, env, {
|
||||
executionContext: context, config: config
|
||||
});
|
||||
},
|
||||
async fetch(request, env, context) {
|
||||
let resp = await router.handle(request, env, {
|
||||
executionContext: context, config: config
|
||||
});
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (config.cors === true) {
|
||||
resp = corsify(resp);
|
||||
}
|
||||
openapi.get("/api/server/config", GetInfo);
|
||||
|
||||
return resp;
|
||||
}
|
||||
};
|
||||
openapi.get("/api/buckets", ListBuckets);
|
||||
openapi.get("/api/buckets/:bucket", ListObjects);
|
||||
openapi.post("/api/buckets/:bucket/move", MoveObject);
|
||||
openapi.post("/api/buckets/:bucket/folder", CreateFolder);
|
||||
openapi.post("/api/buckets/:bucket/upload", PutObject);
|
||||
openapi.post("/api/buckets/:bucket/multipart/create", CreateUpload);
|
||||
openapi.post("/api/buckets/:bucket/multipart/upload", PartUpload);
|
||||
openapi.post("/api/buckets/:bucket/multipart/complete", CompleteUpload);
|
||||
openapi.post("/api/buckets/:bucket/delete", DeleteObject);
|
||||
openapi.on("head", "/api/buckets/:bucket/:key", HeadObject);
|
||||
openapi.get("/api/buckets/:bucket/:key/head", HeadObject); // There are some issues with calling the head method
|
||||
openapi.get("/api/buckets/:bucket/:key", GetObject);
|
||||
openapi.post("/api/buckets/:bucket/:key", PutMetadata);
|
||||
|
||||
openapi.post("/api/emails/send", SendEmail);
|
||||
|
||||
app.get("*", dashboardProxy);
|
||||
|
||||
app.all("*", () =>
|
||||
Response.json({ msg: "404, not found!" }, { status: 404 }),
|
||||
);
|
||||
|
||||
return {
|
||||
// TODO: improve event type
|
||||
async email(
|
||||
event: { raw: unknown; rawSize: unknown },
|
||||
env: AppEnv,
|
||||
context: ExecutionContext,
|
||||
) {
|
||||
await receiveEmail(event, env, context, config);
|
||||
},
|
||||
async fetch(request: Request, env: AppEnv, context: ExecutionContext) {
|
||||
return app.fetch(request, env, context);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
export interface BasicAuth {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface R2ExplorerConfig {
|
||||
readonly?: boolean
|
||||
cors?: boolean
|
||||
cfAccessTeamName?: string
|
||||
dashboardUrl?: string
|
||||
emailRouting?: {
|
||||
targetBucket: string
|
||||
},
|
||||
showHiddenFiles?: boolean
|
||||
cacheAssets?: boolean
|
||||
basicAuth?: BasicAuth | BasicAuth[]
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
config: R2ExplorerConfig
|
||||
username?: string
|
||||
executionContext: ExecutionContext
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class CreateFolder extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-bucket-create-folder",
|
||||
tags: ["Buckets"],
|
||||
summary: "Create folder",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
const key = decodeURIComponent(escape(atob(data.body.key)));
|
||||
|
||||
return await bucket.put(key, "R2 Explorer Folder");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class DeleteObject extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-bucket-delete-object",
|
||||
tags: ["Buckets"],
|
||||
summary: "Delete object",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
const key = decodeURIComponent(escape(atob(data.body.key)));
|
||||
|
||||
await bucket.delete(key);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class GetObject extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "get-bucket-object",
|
||||
tags: ["Buckets"],
|
||||
summary: "Get Object",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
"200": {
|
||||
description: "File binary",
|
||||
schema: z.string().openapi({ format: "binary" }),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
let filePath;
|
||||
try {
|
||||
filePath = decodeURIComponent(escape(atob(data.params.key)));
|
||||
} catch (e) {
|
||||
filePath = decodeURIComponent(
|
||||
escape(atob(decodeURIComponent(data.params.key))),
|
||||
);
|
||||
}
|
||||
|
||||
const object = await bucket.get(filePath);
|
||||
|
||||
if (object === null) {
|
||||
return Response.json({ msg: "Object Not Found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
object.writeHttpMetadata(headers);
|
||||
headers.set("etag", object.httpEtag);
|
||||
headers.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${filePath.split("/").pop()}"`,
|
||||
);
|
||||
|
||||
return new Response(object.body, {
|
||||
headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class HeadObject extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "Head-bucket-object",
|
||||
tags: ["Buckets"],
|
||||
summary: "Get Object",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
let filePath;
|
||||
try {
|
||||
filePath = decodeURIComponent(escape(atob(data.params.key)));
|
||||
} catch (e) {
|
||||
filePath = decodeURIComponent(
|
||||
escape(atob(decodeURIComponent(data.params.key))),
|
||||
);
|
||||
}
|
||||
|
||||
const object = await bucket.head(filePath);
|
||||
|
||||
if (object === null) {
|
||||
return Response.json({ msg: "Object Not Found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class ListBuckets extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "get-bucket-list",
|
||||
tags: ["Buckets"],
|
||||
summary: "List buckets",
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const buckets = [];
|
||||
|
||||
for (const [key, value] of Object.entries(c.env)) {
|
||||
// @ts-ignore - check if the field in Env is actually a R2 bucket by its properties
|
||||
if (value.get && value.put) {
|
||||
buckets.push({ name: key });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buckets: buckets,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class ListObjects extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "get-bucket-list-objects",
|
||||
tags: ["Buckets"],
|
||||
summary: "List objects",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
query: z.object({
|
||||
limit: z.number().optional(),
|
||||
prefix: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe("base64 encoded prefix"),
|
||||
cursor: z.string().nullable().optional(),
|
||||
delimiter: z.string().nullable().optional(),
|
||||
startAfter: z.string().nullable().optional(),
|
||||
include: z.enum(["httpMetadata", "customMetadata"]).array().optional(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
return await bucket.list({
|
||||
limit: data.query.limit,
|
||||
prefix: data.query.prefix
|
||||
? decodeURIComponent(escape(atob(data.query.prefix)))
|
||||
: undefined,
|
||||
cursor: data.query.cursor,
|
||||
startAfter: data.query.startAfter,
|
||||
delimiter: data.query.delimiter ? data.query.delimiter : "",
|
||||
// @ts-ignore
|
||||
include: data.query.include,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class MoveObject extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-bucket-move-object",
|
||||
tags: ["Buckets"],
|
||||
summary: "Move object",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
oldKey: z.string().describe("base64 encoded file key"),
|
||||
newKey: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
const oldKey = decodeURIComponent(escape(atob(data.body.oldKey)));
|
||||
const newKey = decodeURIComponent(escape(atob(data.body.newKey)));
|
||||
|
||||
const object = await bucket.get(oldKey);
|
||||
const resp = await bucket.put(newKey, object.body, {
|
||||
customMetadata: object.customMetadata,
|
||||
httpMetadata: object.httpMetadata,
|
||||
});
|
||||
|
||||
await bucket.delete(oldKey);
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../../types";
|
||||
|
||||
export class CompleteUpload extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-multipart-complete-upload",
|
||||
tags: ["Multipart"],
|
||||
summary: "Complete upload",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
uploadId: z.string(),
|
||||
parts: z
|
||||
.object({
|
||||
etag: z.string(),
|
||||
partNumber: z.number().int(),
|
||||
})
|
||||
.array(),
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
const uploadId = data.body.uploadId;
|
||||
const key = decodeURIComponent(escape(atob(data.body.key)));
|
||||
const parts = data.body.parts;
|
||||
|
||||
const multipartUpload = await bucket.resumeMultipartUpload(key, uploadId);
|
||||
|
||||
try {
|
||||
const resp = await multipartUpload.complete(parts);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
str: resp,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return Response.json({ msg: error.message }, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../../types";
|
||||
|
||||
export class CreateUpload extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-multipart-create-upload",
|
||||
tags: ["Multipart"],
|
||||
summary: "Create upload",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
query: z.object({
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
customMetadata: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe("base64 encoded json string"),
|
||||
httpMetadata: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe("base64 encoded json string"),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
const key = decodeURIComponent(escape(atob(data.query.key)));
|
||||
|
||||
let customMetadata = undefined;
|
||||
if (data.query.customMetadata) {
|
||||
customMetadata = JSON.parse(
|
||||
decodeURIComponent(escape(atob(data.query.customMetadata))),
|
||||
);
|
||||
}
|
||||
|
||||
let httpMetadata = undefined;
|
||||
if (data.query.httpMetadata) {
|
||||
httpMetadata = JSON.parse(
|
||||
decodeURIComponent(escape(atob(data.query.httpMetadata))),
|
||||
);
|
||||
}
|
||||
|
||||
return await bucket.createMultipartUpload(key, {
|
||||
customMetadata: customMetadata,
|
||||
httpMetadata: httpMetadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../../types";
|
||||
|
||||
export class PartUpload extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-multipart-part-upload",
|
||||
tags: ["Multipart"],
|
||||
summary: "Part upload",
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/octet-stream": {
|
||||
schema: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
query: z.object({
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
uploadId: z.string(),
|
||||
partNumber: z.number().int(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
const key = decodeURIComponent(escape(atob(data.query.key)));
|
||||
|
||||
const multipartUpload = bucket.resumeMultipartUpload(
|
||||
key,
|
||||
data.query.uploadId,
|
||||
);
|
||||
|
||||
try {
|
||||
return await multipartUpload.uploadPart(
|
||||
data.query.partNumber,
|
||||
c.req.raw.body,
|
||||
);
|
||||
} catch (error: any) {
|
||||
return new Response(error.message, { status: 400 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class PutMetadata extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-bucket-put-object-metadata",
|
||||
tags: ["Buckets"],
|
||||
summary: "Update object metadata",
|
||||
request: {
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z
|
||||
.object({
|
||||
customMetadata: z.record(z.string(), z.any()),
|
||||
})
|
||||
.openapi("Object metadata"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
let filePath;
|
||||
try {
|
||||
filePath = decodeURIComponent(escape(atob(data.params.key)));
|
||||
} catch (e) {
|
||||
filePath = decodeURIComponent(
|
||||
escape(atob(decodeURIComponent(data.params.key))),
|
||||
);
|
||||
}
|
||||
|
||||
const object = await bucket.get(filePath);
|
||||
return await bucket.put(filePath, object.body, {
|
||||
customMetadata: data.body.customMetadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class PutObject extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-bucket-upload-object",
|
||||
tags: ["Buckets"],
|
||||
summary: "Upload object",
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/octet-stream": {
|
||||
schema: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: z.object({
|
||||
bucket: z.string(),
|
||||
}),
|
||||
query: z.object({
|
||||
key: z.string().describe("base64 encoded file key"),
|
||||
customMetadata: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe("base64 encoded json string"),
|
||||
httpMetadata: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
.describe("base64 encoded json string"),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const data = await this.getValidatedData<typeof this.schema>();
|
||||
|
||||
const bucket = c.env[data.params.bucket];
|
||||
|
||||
const key = decodeURIComponent(escape(atob(data.query.key)));
|
||||
|
||||
let customMetadata = undefined;
|
||||
if (data.query.customMetadata) {
|
||||
customMetadata = JSON.parse(
|
||||
decodeURIComponent(escape(atob(data.query.customMetadata))),
|
||||
);
|
||||
}
|
||||
|
||||
let httpMetadata = undefined;
|
||||
if (data.query.httpMetadata) {
|
||||
httpMetadata = JSON.parse(
|
||||
decodeURIComponent(escape(atob(data.query.httpMetadata))),
|
||||
);
|
||||
}
|
||||
|
||||
return await bucket.put(key, c.req.raw.body, {
|
||||
customMetadata: customMetadata,
|
||||
httpMetadata: httpMetadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { ExecutionContext } from "hono";
|
||||
import PostalMime from "postal-mime";
|
||||
import { getCurrentTimestampMilliseconds } from "../../foundation/dates";
|
||||
import type { AppEnv, R2ExplorerConfig } from "../../types";
|
||||
|
||||
async function streamToArrayBuffer(stream, streamSize) {
|
||||
const result = new Uint8Array(streamSize);
|
||||
let bytesRead = 0;
|
||||
const reader = stream.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
result.set(value, bytesRead);
|
||||
bytesRead += value.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function receiveEmail(
|
||||
event: { raw: unknown; rawSize: unknown },
|
||||
env: AppEnv,
|
||||
ctx: ExecutionContext,
|
||||
config: R2ExplorerConfig,
|
||||
) {
|
||||
let bucket;
|
||||
|
||||
if (
|
||||
config?.emailRouting?.targetBucket &&
|
||||
env[config.emailRouting.targetBucket]
|
||||
) {
|
||||
bucket = env[config.emailRouting.targetBucket];
|
||||
}
|
||||
|
||||
if (!bucket) {
|
||||
// Bucket not set, default to first defined
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
// @ts-ignore
|
||||
if (value.get && value.put) {
|
||||
bucket = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rawEmail = await streamToArrayBuffer(event.raw, event.rawSize);
|
||||
const parser = new PostalMime();
|
||||
const parsedEmail = await parser.parse(rawEmail);
|
||||
|
||||
const emailPath = `${getCurrentTimestampMilliseconds()}-${crypto.randomUUID()}`;
|
||||
|
||||
await bucket.put(
|
||||
`.r2-explorer/emails/inbox/${emailPath}.json`,
|
||||
JSON.stringify(parsedEmail),
|
||||
{
|
||||
customMetadata: {
|
||||
subject: parsedEmail.subject,
|
||||
from_address: parsedEmail.from?.address,
|
||||
from_name: parsedEmail.from?.name,
|
||||
to_address:
|
||||
parsedEmail.to.length > 0 ? parsedEmail.to[0].address : null,
|
||||
to_name: parsedEmail.to.length > 0 ? parsedEmail.to[0].name : null,
|
||||
has_attachments: parsedEmail.attachments.length > 0,
|
||||
read: false,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
for (const att of parsedEmail.attachments) {
|
||||
await bucket.put(
|
||||
`.r2-explorer/emails/inbox/${emailPath}/${att.filename}`,
|
||||
att.content,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { OpenAPIRoute, Str } from "chanfana";
|
||||
import { z } from "zod";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class SendEmail extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "post-email-send",
|
||||
tags: ["Emails"],
|
||||
summary: "Send Email",
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
subject: Str({ example: "Look! No servers" }),
|
||||
from: z.object({
|
||||
email: Str({ example: "[email protected]" }),
|
||||
name: Str({ example: "Workers - MailChannels integration" }),
|
||||
}),
|
||||
to: z
|
||||
.object({
|
||||
email: Str({ example: "[email protected]" }),
|
||||
name: Str({ example: "Test Recipient" }),
|
||||
})
|
||||
.array(),
|
||||
content: z.object({}).catchall(z.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
if (c.get("config").readonly === true)
|
||||
return Response.json({ msg: "unauthorized" }, { status: 401 });
|
||||
|
||||
// TODO: re-enable this with cloudflare workers
|
||||
// const emailResp = await fetch("https://api.mailchannels.net/tx/v1/send", {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "content-type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// personalizations: [
|
||||
// {
|
||||
// to: data.body.to,
|
||||
// },
|
||||
// ],
|
||||
// from: data.body.from,
|
||||
// subject: data.body.subject,
|
||||
// content: [
|
||||
// {
|
||||
// type: "text/plain",
|
||||
// value: data.body.content["text/plain"],
|
||||
// },
|
||||
// ],
|
||||
// }),
|
||||
// });
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "unavailable",
|
||||
};
|
||||
|
||||
// return { msg: await emailResp.text() };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { OpenAPIRoute } from "chanfana";
|
||||
import { settings } from "../../foundation/settings";
|
||||
import type { AppContext } from "../../types";
|
||||
|
||||
export class GetInfo extends OpenAPIRoute {
|
||||
schema = {
|
||||
operationId: "get-server-info",
|
||||
tags: ["Server"],
|
||||
summary: "Get server info",
|
||||
};
|
||||
|
||||
async handle(c: AppContext) {
|
||||
const { basicAuth, ...config } = c.get("config");
|
||||
|
||||
return {
|
||||
version: settings.version,
|
||||
config: config,
|
||||
user: {
|
||||
username: c.get("username"),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import {OpenAPIRoute} from "@cloudflare/itty-router-openapi";
|
||||
import {Context} from "../../interfaces";
|
||||
import {OpenAPIRouteSchema} from "@cloudflare/itty-router-openapi/dist/src/types";
|
||||
import {config} from "../../settings";
|
||||
|
||||
export class GetInfo extends OpenAPIRoute {
|
||||
static schema: OpenAPIRouteSchema = {
|
||||
operationId: 'get-server-info',
|
||||
tags: ['Server'],
|
||||
summary: 'Get server info',
|
||||
}
|
||||
|
||||
async handle(
|
||||
request: Request,
|
||||
env: any,
|
||||
context: Context,
|
||||
data: any
|
||||
) {
|
||||
const serverConfig = {...context.config}
|
||||
delete serverConfig.basicAuth
|
||||
|
||||
return {
|
||||
version: config.version,
|
||||
config: serverConfig,
|
||||
user: {
|
||||
username: context.username
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import {config} from "../settings";
|
||||
import {GetInfo} from "./api/getInfo";
|
||||
import {OpenAPIRouter} from "@cloudflare/itty-router-openapi";
|
||||
|
||||
|
||||
export const serverRouter = OpenAPIRouter({
|
||||
base: '/api/server',
|
||||
raiseUnknownParameters: config.raiseUnknownParameters,
|
||||
generateOperationIds: config.generateOperationIds,
|
||||
})
|
||||
|
||||
serverRouter.get('/config', GetInfo)
|
||||
@@ -1,5 +0,0 @@
|
||||
export const config = {
|
||||
raiseUnknownParameters: true,
|
||||
generateOperationIds: false,
|
||||
version: '0.0.1'
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import type { Context } from "hono";
|
||||
|
||||
export type BasicAuth = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type R2ExplorerConfig = {
|
||||
readonly?: boolean;
|
||||
cors?: boolean;
|
||||
cfAccessTeamName?: string;
|
||||
dashboardUrl?: string;
|
||||
emailRouting?: {
|
||||
targetBucket: string;
|
||||
};
|
||||
showHiddenFiles?: boolean;
|
||||
cacheAssets?: boolean;
|
||||
basicAuth?: BasicAuth | BasicAuth[];
|
||||
};
|
||||
|
||||
export type AppEnv = {
|
||||
[key: string]: R2Bucket;
|
||||
};
|
||||
export type AppVariables = {
|
||||
config: R2ExplorerConfig;
|
||||
username?: string;
|
||||
};
|
||||
export type AppContext = Context<{ Bindings: AppEnv; Variables: AppVariables }>;
|
||||
@@ -1,29 +0,0 @@
|
||||
/* eslint-disable */
|
||||
const fs = require('fs')
|
||||
const Path = require('path')
|
||||
/* eslint-enable */
|
||||
|
||||
const deleteFolderRecursive = (path) => {
|
||||
if (fs.existsSync(path)) {
|
||||
fs.readdirSync(path).forEach((file) => {
|
||||
const curPath = Path.join(path, file)
|
||||
if (fs.lstatSync(curPath).isDirectory()) {
|
||||
deleteFolderRecursive(curPath)
|
||||
} else {
|
||||
fs.unlinkSync(curPath)
|
||||
}
|
||||
})
|
||||
fs.rmdirSync(path)
|
||||
}
|
||||
}
|
||||
|
||||
const folder = process.argv.slice(2)[0]
|
||||
|
||||
if (folder) {
|
||||
deleteFolderRecursive(Path.join(__dirname, '../dist', folder))
|
||||
} else {
|
||||
deleteFolderRecursive(Path.join(__dirname, '../dist/cjs'))
|
||||
deleteFolderRecursive(Path.join(__dirname, '../dist/esm'))
|
||||
deleteFolderRecursive(Path.join(__dirname, '../dist/umd'))
|
||||
deleteFolderRecursive(Path.join(__dirname, '../dist/types'))
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* eslint-disable */
|
||||
const fs = require('fs')
|
||||
const Path = require('path')
|
||||
const fileName = '../package.json'
|
||||
const file = require(fileName)
|
||||
/* eslint-enable */
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
for (let i = 0, l = args.length; i < l; i++) {
|
||||
if (i % 2 === 0) {
|
||||
file[args[i]] = args[i + 1]
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFile(Path.join(__dirname, fileName), JSON.stringify(file, null, 2), (err) => {
|
||||
if (err) {
|
||||
return console.log(err)
|
||||
}
|
||||
console.log('Writing to ' + fileName)
|
||||
})
|
||||
@@ -1,28 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": "src",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"inlineSourceMap": false,
|
||||
"lib": ["esnext"],
|
||||
"listEmittedFiles": false,
|
||||
"listFiles": false,
|
||||
"moduleResolution": "node",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"pretty": true,
|
||||
"resolveJsonModule": true,
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"traceResolution": false,
|
||||
"outDir": "",
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"types": ["@types/node", "@types/service-worker-mock", "@cloudflare/workers-types"]
|
||||
},
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"include": ["src", "scripts"]
|
||||
"compilerOptions": {
|
||||
"target": "esNext",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": true,
|
||||
"types": ["@types/node", "@cloudflare/workers-types"]
|
||||
},
|
||||
"include": ["src/*.ts", "src/**/*.ts"]
|
||||
}
|
||||
|
||||
Generated
+3619
-4123
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user