From a3f1fd6a4c8b89e3c232784e61caa369f24dc16f Mon Sep 17 00:00:00 2001 From: Lan Date: Mon, 1 Dec 2025 14:10:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=A9=E7=94=A8AI=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- core/settings.py | 1 - docs/.vitepress/cache/deps/@theme_index.js | 275 ---------- .../.vitepress/cache/deps/@theme_index.js.map | 7 - docs/.vitepress/cache/deps/_metadata.json | 24 +- docs/.vitepress/theme/custom.css | 473 ++++++++++++++++++ docs/.vitepress/theme/index.ts | 6 + docs/en/guide/configuration.md | 207 ++++++++ docs/en/guide/management.md | 413 +++++++++++++++ docs/en/guide/security.md | 325 ++++++++++++ docs/en/guide/share.md | 342 +++++++++++++ docs/en/guide/storage.md | 395 +++++++++++++++ docs/en/guide/upload copy.md | 0 docs/en/guide/upload.md | 380 ++++++++++++++ docs/guide/configuration.md | 206 ++++++++ docs/guide/management.md | 412 +++++++++++++++ docs/guide/security.md | 324 ++++++++++++ docs/guide/share.md | 341 +++++++++++++ docs/guide/storage.md | 397 ++++++++++++++- docs/guide/upload.md | 379 ++++++++++++++ 20 files changed, 4598 insertions(+), 313 deletions(-) delete mode 100644 docs/.vitepress/cache/deps/@theme_index.js delete mode 100644 docs/.vitepress/cache/deps/@theme_index.js.map create mode 100644 docs/.vitepress/theme/custom.css create mode 100644 docs/.vitepress/theme/index.ts delete mode 100644 docs/en/guide/upload copy.md diff --git a/.gitignore b/.gitignore index db9bc5c..5fffab0 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,6 @@ data/.env node_modules/ -AGENTS.md \ No newline at end of file +AGENTS.md + +dist/ \ No newline at end of file diff --git a/core/settings.py b/core/settings.py index 7190bd5..328e247 100644 --- a/core/settings.py +++ b/core/settings.py @@ -35,7 +35,6 @@ DEFAULT_CONFIG = { "onedrive_password": "", "onedrive_root_path": "filebox_storage", "onedrive_proxy": 0, - "webdav_hostname": "", "webdav_root_path": "filebox_storage", "webdav_proxy": 0, "admin_token": "FileCodeBox2023", diff --git a/docs/.vitepress/cache/deps/@theme_index.js b/docs/.vitepress/cache/deps/@theme_index.js deleted file mode 100644 index c8dea83..0000000 --- a/docs/.vitepress/cache/deps/@theme_index.js +++ /dev/null @@ -1,275 +0,0 @@ -import { - useMediaQuery -} from "./chunk-KT7LHMJ2.js"; -import { - computed, - ref, - shallowRef, - watch -} from "./chunk-CQOUZRMK.js"; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/index.js -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/fonts.css"; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/without-fonts.js -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/vars.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/base.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/icons.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/utils.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css"; -import "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css"; -import VPBadge from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue"; -import Layout from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/Layout.vue"; -import { default as default2 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue"; -import { default as default3 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue"; -import { default as default4 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue"; -import { default as default5 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue"; -import { default as default6 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue"; -import { default as default7 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue"; -import { default as default8 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue"; -import { default as default9 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue"; -import { default as default10 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue"; -import { default as default11 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue"; -import { default as default12 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue"; -import { default as default13 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue"; -import { default as default14 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue"; -import { default as default15 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue"; -import { default as default16 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue"; -import { default as default17 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue"; -import { default as default18 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue"; -import { default as default19 } from "/Users/lan/Project/Python/FileCodeBox/docs/node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue"; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/local-nav.js -import { onContentUpdated } from "vitepress"; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/outline.js -import { getScrollOffset } from "vitepress"; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/support/utils.js -import { withBase } from "vitepress"; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/data.js -import { useData as useData$ } from "vitepress"; -var useData = useData$; - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/support/utils.js -function ensureStartingSlash(path) { - return path.startsWith("/") ? path : `/${path}`; -} - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/support/sidebar.js -function getSidebar(_sidebar, path) { - if (Array.isArray(_sidebar)) - return addBase(_sidebar); - if (_sidebar == null) - return []; - path = ensureStartingSlash(path); - const dir = Object.keys(_sidebar).sort((a, b) => { - return b.split("/").length - a.split("/").length; - }).find((dir2) => { - return path.startsWith(ensureStartingSlash(dir2)); - }); - const sidebar = dir ? _sidebar[dir] : []; - return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base); -} -function getSidebarGroups(sidebar) { - const groups = []; - let lastGroupIndex = 0; - for (const index in sidebar) { - const item = sidebar[index]; - if (item.items) { - lastGroupIndex = groups.push(item); - continue; - } - if (!groups[lastGroupIndex]) { - groups.push({ items: [] }); - } - groups[lastGroupIndex].items.push(item); - } - return groups; -} -function addBase(items, _base) { - return [...items].map((_item) => { - const item = { ..._item }; - const base = item.base || _base; - if (base && item.link) - item.link = base + item.link; - if (item.items) - item.items = addBase(item.items, base); - return item; - }); -} - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/sidebar.js -function useSidebar() { - const { frontmatter, page, theme: theme2 } = useData(); - const is960 = useMediaQuery("(min-width: 960px)"); - const isOpen = ref(false); - const _sidebar = computed(() => { - const sidebarConfig = theme2.value.sidebar; - const relativePath = page.value.relativePath; - return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : []; - }); - const sidebar = ref(_sidebar.value); - watch(_sidebar, (next, prev) => { - if (JSON.stringify(next) !== JSON.stringify(prev)) - sidebar.value = _sidebar.value; - }); - const hasSidebar = computed(() => { - return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home"; - }); - const leftAside = computed(() => { - if (hasAside) - return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left"; - return false; - }); - const hasAside = computed(() => { - if (frontmatter.value.layout === "home") - return false; - if (frontmatter.value.aside != null) - return !!frontmatter.value.aside; - return theme2.value.aside !== false; - }); - const isSidebarEnabled = computed(() => hasSidebar.value && is960.value); - const sidebarGroups = computed(() => { - return hasSidebar.value ? getSidebarGroups(sidebar.value) : []; - }); - function open() { - isOpen.value = true; - } - function close() { - isOpen.value = false; - } - function toggle() { - isOpen.value ? close() : open(); - } - return { - isOpen, - sidebar, - sidebarGroups, - hasSidebar, - hasAside, - leftAside, - isSidebarEnabled, - open, - close, - toggle - }; -} - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/outline.js -var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/; -var resolvedHeaders = []; -function getHeaders(range) { - const headers = [ - ...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)") - ].filter((el) => el.id && el.hasChildNodes()).map((el) => { - const level = Number(el.tagName[1]); - return { - element: el, - title: serializeHeader(el), - link: "#" + el.id, - level - }; - }); - return resolveHeaders(headers, range); -} -function serializeHeader(h) { - let ret = ""; - for (const node of h.childNodes) { - if (node.nodeType === 1) { - if (ignoreRE.test(node.className)) - continue; - ret += node.textContent; - } else if (node.nodeType === 3) { - ret += node.textContent; - } - } - return ret.trim(); -} -function resolveHeaders(headers, range) { - if (range === false) { - return []; - } - const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2; - const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange; - return buildTree(headers, high, low); -} -function buildTree(data, min, max) { - resolvedHeaders.length = 0; - const result = []; - const stack = []; - data.forEach((item) => { - const node = { ...item, children: [] }; - let parent = stack[stack.length - 1]; - while (parent && parent.level >= node.level) { - stack.pop(); - parent = stack[stack.length - 1]; - } - if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) { - stack.push({ level: node.level, shouldIgnore: true }); - return; - } - if (node.level > max || node.level < min) - return; - resolvedHeaders.push({ element: node.element, link: node.link }); - if (parent) - parent.children.push(node); - else - result.push(node); - stack.push(node); - }); - return result; -} - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/local-nav.js -function useLocalNav() { - const { theme: theme2, frontmatter } = useData(); - const headers = shallowRef([]); - const hasLocalNav = computed(() => { - return headers.value.length > 0; - }); - onContentUpdated(() => { - headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline); - }); - return { - headers, - hasLocalNav - }; -} - -// node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/without-fonts.js -var theme = { - Layout, - enhanceApp: ({ app }) => { - app.component("Badge", VPBadge); - } -}; -var without_fonts_default = theme; -export { - default2 as VPBadge, - default3 as VPButton, - default4 as VPDocAsideSponsors, - default5 as VPFeatures, - default6 as VPHomeContent, - default7 as VPHomeFeatures, - default8 as VPHomeHero, - default9 as VPHomeSponsors, - default10 as VPImage, - default11 as VPLink, - default12 as VPNavBarSearch, - default13 as VPSocialLink, - default14 as VPSocialLinks, - default15 as VPSponsors, - default16 as VPTeamMembers, - default17 as VPTeamPage, - default18 as VPTeamPageSection, - default19 as VPTeamPageTitle, - without_fonts_default as default, - useLocalNav, - useSidebar -}; -//# sourceMappingURL=@theme_index.js.map diff --git a/docs/.vitepress/cache/deps/@theme_index.js.map b/docs/.vitepress/cache/deps/@theme_index.js.map deleted file mode 100644 index e4a66a1..0000000 --- a/docs/.vitepress/cache/deps/@theme_index.js.map +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": 3, - "sources": ["../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/index.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/without-fonts.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/local-nav.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/outline.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/support/utils.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/data.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/support/sidebar.js", "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/composables/sidebar.js"], - "sourcesContent": ["import './styles/fonts.css';\nexport * from './without-fonts';\nexport { default as default } from './without-fonts';\n", "import './styles/vars.css';\nimport './styles/base.css';\nimport './styles/icons.css';\nimport './styles/utils.css';\nimport './styles/components/custom-block.css';\nimport './styles/components/vp-code.css';\nimport './styles/components/vp-code-group.css';\nimport './styles/components/vp-doc.css';\nimport './styles/components/vp-sponsor.css';\nimport VPBadge from './components/VPBadge.vue';\nimport Layout from './Layout.vue';\nexport { default as VPBadge } from './components/VPBadge.vue';\nexport { default as VPButton } from './components/VPButton.vue';\nexport { default as VPDocAsideSponsors } from './components/VPDocAsideSponsors.vue';\nexport { default as VPFeatures } from './components/VPFeatures.vue';\nexport { default as VPHomeContent } from './components/VPHomeContent.vue';\nexport { default as VPHomeFeatures } from './components/VPHomeFeatures.vue';\nexport { default as VPHomeHero } from './components/VPHomeHero.vue';\nexport { default as VPHomeSponsors } from './components/VPHomeSponsors.vue';\nexport { default as VPImage } from './components/VPImage.vue';\nexport { default as VPLink } from './components/VPLink.vue';\nexport { default as VPNavBarSearch } from './components/VPNavBarSearch.vue';\nexport { default as VPSocialLink } from './components/VPSocialLink.vue';\nexport { default as VPSocialLinks } from './components/VPSocialLinks.vue';\nexport { default as VPSponsors } from './components/VPSponsors.vue';\nexport { default as VPTeamMembers } from './components/VPTeamMembers.vue';\nexport { default as VPTeamPage } from './components/VPTeamPage.vue';\nexport { default as VPTeamPageSection } from './components/VPTeamPageSection.vue';\nexport { default as VPTeamPageTitle } from './components/VPTeamPageTitle.vue';\nexport { useLocalNav } from './composables/local-nav';\nexport { useSidebar } from './composables/sidebar';\nconst theme = {\n Layout,\n enhanceApp: ({ app }) => {\n app.component('Badge', VPBadge);\n }\n};\nexport default theme;\n", "import { onContentUpdated } from 'vitepress';\nimport { computed, shallowRef } from 'vue';\nimport { getHeaders } from '../composables/outline';\nimport { useData } from './data';\nexport function useLocalNav() {\n const { theme, frontmatter } = useData();\n const headers = shallowRef([]);\n const hasLocalNav = computed(() => {\n return headers.value.length > 0;\n });\n onContentUpdated(() => {\n headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline);\n });\n return {\n headers,\n hasLocalNav\n };\n}\n", "import { getScrollOffset } from 'vitepress';\nimport { onMounted, onUnmounted, onUpdated } from 'vue';\nimport { throttleAndDebounce } from '../support/utils';\nimport { useAside } from './aside';\nconst ignoreRE = /\\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\\b/;\n// cached list of anchor elements from resolveHeaders\nconst resolvedHeaders = [];\nexport function resolveTitle(theme) {\n return ((typeof theme.outline === 'object' &&\n !Array.isArray(theme.outline) &&\n theme.outline.label) ||\n theme.outlineTitle ||\n 'On this page');\n}\nexport function getHeaders(range) {\n const headers = [\n ...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)')\n ]\n .filter((el) => el.id && el.hasChildNodes())\n .map((el) => {\n const level = Number(el.tagName[1]);\n return {\n element: el,\n title: serializeHeader(el),\n link: '#' + el.id,\n level\n };\n });\n return resolveHeaders(headers, range);\n}\nfunction serializeHeader(h) {\n let ret = '';\n for (const node of h.childNodes) {\n if (node.nodeType === 1) {\n if (ignoreRE.test(node.className))\n continue;\n ret += node.textContent;\n }\n else if (node.nodeType === 3) {\n ret += node.textContent;\n }\n }\n return ret.trim();\n}\nexport function resolveHeaders(headers, range) {\n if (range === false) {\n return [];\n }\n const levelsRange = (typeof range === 'object' && !Array.isArray(range)\n ? range.level\n : range) || 2;\n const [high, low] = typeof levelsRange === 'number'\n ? [levelsRange, levelsRange]\n : levelsRange === 'deep'\n ? [2, 6]\n : levelsRange;\n return buildTree(headers, high, low);\n}\nexport function useActiveAnchor(container, marker) {\n const { isAsideEnabled } = useAside();\n const onScroll = throttleAndDebounce(setActiveLink, 100);\n let prevActiveLink = null;\n onMounted(() => {\n requestAnimationFrame(setActiveLink);\n window.addEventListener('scroll', onScroll);\n });\n onUpdated(() => {\n // sidebar update means a route change\n activateLink(location.hash);\n });\n onUnmounted(() => {\n window.removeEventListener('scroll', onScroll);\n });\n function setActiveLink() {\n if (!isAsideEnabled.value) {\n return;\n }\n const scrollY = window.scrollY;\n const innerHeight = window.innerHeight;\n const offsetHeight = document.body.offsetHeight;\n const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;\n // resolvedHeaders may be repositioned, hidden or fix positioned\n const headers = resolvedHeaders\n .map(({ element, link }) => ({\n link,\n top: getAbsoluteTop(element)\n }))\n .filter(({ top }) => !Number.isNaN(top))\n .sort((a, b) => a.top - b.top);\n // no headers available for active link\n if (!headers.length) {\n activateLink(null);\n return;\n }\n // page top\n if (scrollY < 1) {\n activateLink(null);\n return;\n }\n // page bottom - highlight last link\n if (isBottom) {\n activateLink(headers[headers.length - 1].link);\n return;\n }\n // find the last header above the top of viewport\n let activeLink = null;\n for (const { link, top } of headers) {\n if (top > scrollY + getScrollOffset() + 4) {\n break;\n }\n activeLink = link;\n }\n activateLink(activeLink);\n }\n function activateLink(hash) {\n if (prevActiveLink) {\n prevActiveLink.classList.remove('active');\n }\n if (hash == null) {\n prevActiveLink = null;\n }\n else {\n prevActiveLink = container.value.querySelector(`a[href=\"${decodeURIComponent(hash)}\"]`);\n }\n const activeLink = prevActiveLink;\n if (activeLink) {\n activeLink.classList.add('active');\n marker.value.style.top = activeLink.offsetTop + 39 + 'px';\n marker.value.style.opacity = '1';\n }\n else {\n marker.value.style.top = '33px';\n marker.value.style.opacity = '0';\n }\n }\n}\nfunction getAbsoluteTop(element) {\n let offsetTop = 0;\n while (element !== document.body) {\n if (element === null) {\n // child element is:\n // - not attached to the DOM (display: none)\n // - set to fixed position (not scrollable)\n // - body or html element (null offsetParent)\n return NaN;\n }\n offsetTop += element.offsetTop;\n element = element.offsetParent;\n }\n return offsetTop;\n}\nfunction buildTree(data, min, max) {\n resolvedHeaders.length = 0;\n const result = [];\n const stack = [];\n data.forEach((item) => {\n const node = { ...item, children: [] };\n let parent = stack[stack.length - 1];\n while (parent && parent.level >= node.level) {\n stack.pop();\n parent = stack[stack.length - 1];\n }\n if (node.element.classList.contains('ignore-header') ||\n (parent && 'shouldIgnore' in parent)) {\n stack.push({ level: node.level, shouldIgnore: true });\n return;\n }\n if (node.level > max || node.level < min)\n return;\n resolvedHeaders.push({ element: node.element, link: node.link });\n if (parent)\n parent.children.push(node);\n else\n result.push(node);\n stack.push(node);\n });\n return result;\n}\n", "import { withBase } from 'vitepress';\nimport { isExternal, treatAsHtml } from '../../shared';\nimport { useData } from '../composables/data';\nexport function throttleAndDebounce(fn, delay) {\n let timeoutId;\n let called = false;\n return () => {\n if (timeoutId)\n clearTimeout(timeoutId);\n if (!called) {\n fn();\n (called = true) && setTimeout(() => (called = false), delay);\n }\n else\n timeoutId = setTimeout(fn, delay);\n };\n}\nexport function ensureStartingSlash(path) {\n return path.startsWith('/') ? path : `/${path}`;\n}\nexport function normalizeLink(url) {\n const { pathname, search, hash, protocol } = new URL(url, 'http://a.com');\n if (isExternal(url) ||\n url.startsWith('#') ||\n !protocol.startsWith('http') ||\n !treatAsHtml(pathname))\n return url;\n const { site } = useData();\n const normalizedPath = pathname.endsWith('/') || pathname.endsWith('.html')\n ? url\n : url.replace(/(?:(^\\.+)\\/)?.*$/, `$1${pathname.replace(/(\\.md)?$/, site.value.cleanUrls ? '' : '.html')}${search}${hash}`);\n return withBase(normalizedPath);\n}\n", "import { useData as useData$ } from 'vitepress';\nexport const useData = useData$;\n", "import { isActive } from '../../shared';\nimport { ensureStartingSlash } from './utils';\n/**\n * Get the `Sidebar` from sidebar option. This method will ensure to get correct\n * sidebar config from `MultiSideBarConfig` with various path combinations such\n * as matching `guide/` and `/guide/`. If no matching config was found, it will\n * return empty array.\n */\nexport function getSidebar(_sidebar, path) {\n if (Array.isArray(_sidebar))\n return addBase(_sidebar);\n if (_sidebar == null)\n return [];\n path = ensureStartingSlash(path);\n const dir = Object.keys(_sidebar)\n .sort((a, b) => {\n return b.split('/').length - a.split('/').length;\n })\n .find((dir) => {\n // make sure the multi sidebar key starts with slash too\n return path.startsWith(ensureStartingSlash(dir));\n });\n const sidebar = dir ? _sidebar[dir] : [];\n return Array.isArray(sidebar)\n ? addBase(sidebar)\n : addBase(sidebar.items, sidebar.base);\n}\n/**\n * Get or generate sidebar group from the given sidebar items.\n */\nexport function getSidebarGroups(sidebar) {\n const groups = [];\n let lastGroupIndex = 0;\n for (const index in sidebar) {\n const item = sidebar[index];\n if (item.items) {\n lastGroupIndex = groups.push(item);\n continue;\n }\n if (!groups[lastGroupIndex]) {\n groups.push({ items: [] });\n }\n groups[lastGroupIndex].items.push(item);\n }\n return groups;\n}\nexport function getFlatSideBarLinks(sidebar) {\n const links = [];\n function recursivelyExtractLinks(items) {\n for (const item of items) {\n if (item.text && item.link) {\n links.push({\n text: item.text,\n link: item.link,\n docFooterText: item.docFooterText\n });\n }\n if (item.items) {\n recursivelyExtractLinks(item.items);\n }\n }\n }\n recursivelyExtractLinks(sidebar);\n return links;\n}\n/**\n * Check if the given sidebar item contains any active link.\n */\nexport function hasActiveLink(path, items) {\n if (Array.isArray(items)) {\n return items.some((item) => hasActiveLink(path, item));\n }\n return isActive(path, items.link)\n ? true\n : items.items\n ? hasActiveLink(path, items.items)\n : false;\n}\nfunction addBase(items, _base) {\n return [...items].map((_item) => {\n const item = { ..._item };\n const base = item.base || _base;\n if (base && item.link)\n item.link = base + item.link;\n if (item.items)\n item.items = addBase(item.items, base);\n return item;\n });\n}\n", "import { useMediaQuery } from '@vueuse/core';\nimport { computed, onMounted, onUnmounted, ref, watch, watchEffect, watchPostEffect } from 'vue';\nimport { isActive } from '../../shared';\nimport { hasActiveLink as containsActiveLink, getSidebar, getSidebarGroups } from '../support/sidebar';\nimport { useData } from './data';\nexport function useSidebar() {\n const { frontmatter, page, theme } = useData();\n const is960 = useMediaQuery('(min-width: 960px)');\n const isOpen = ref(false);\n const _sidebar = computed(() => {\n const sidebarConfig = theme.value.sidebar;\n const relativePath = page.value.relativePath;\n return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];\n });\n const sidebar = ref(_sidebar.value);\n watch(_sidebar, (next, prev) => {\n if (JSON.stringify(next) !== JSON.stringify(prev))\n sidebar.value = _sidebar.value;\n });\n const hasSidebar = computed(() => {\n return (frontmatter.value.sidebar !== false &&\n sidebar.value.length > 0 &&\n frontmatter.value.layout !== 'home');\n });\n const leftAside = computed(() => {\n if (hasAside)\n return frontmatter.value.aside == null\n ? theme.value.aside === 'left'\n : frontmatter.value.aside === 'left';\n return false;\n });\n const hasAside = computed(() => {\n if (frontmatter.value.layout === 'home')\n return false;\n if (frontmatter.value.aside != null)\n return !!frontmatter.value.aside;\n return theme.value.aside !== false;\n });\n const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);\n const sidebarGroups = computed(() => {\n return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];\n });\n function open() {\n isOpen.value = true;\n }\n function close() {\n isOpen.value = false;\n }\n function toggle() {\n isOpen.value ? close() : open();\n }\n return {\n isOpen,\n sidebar,\n sidebarGroups,\n hasSidebar,\n hasAside,\n leftAside,\n isSidebarEnabled,\n open,\n close,\n toggle\n };\n}\n/**\n * a11y: cache the element that opened the Sidebar (the menu button) then\n * focus that button again when Menu is closed with Escape key.\n */\nexport function useCloseSidebarOnEscape(isOpen, close) {\n let triggerElement;\n watchEffect(() => {\n triggerElement = isOpen.value\n ? document.activeElement\n : undefined;\n });\n onMounted(() => {\n window.addEventListener('keyup', onEscape);\n });\n onUnmounted(() => {\n window.removeEventListener('keyup', onEscape);\n });\n function onEscape(e) {\n if (e.key === 'Escape' && isOpen.value) {\n close();\n triggerElement?.focus();\n }\n }\n}\nexport function useSidebarControl(item) {\n const { page, hash } = useData();\n const collapsed = ref(false);\n const collapsible = computed(() => {\n return item.value.collapsed != null;\n });\n const isLink = computed(() => {\n return !!item.value.link;\n });\n const isActiveLink = ref(false);\n const updateIsActiveLink = () => {\n isActiveLink.value = isActive(page.value.relativePath, item.value.link);\n };\n watch([page, item, hash], updateIsActiveLink);\n onMounted(updateIsActiveLink);\n const hasActiveLink = computed(() => {\n if (isActiveLink.value) {\n return true;\n }\n return item.value.items\n ? containsActiveLink(page.value.relativePath, item.value.items)\n : false;\n });\n const hasChildren = computed(() => {\n return !!(item.value.items && item.value.items.length);\n });\n watchEffect(() => {\n collapsed.value = !!(collapsible.value && item.value.collapsed);\n });\n watchPostEffect(() => {\n ;\n (isActiveLink.value || hasActiveLink.value) && (collapsed.value = false);\n });\n function toggle() {\n if (collapsible.value) {\n collapsed.value = !collapsed.value;\n }\n }\n return {\n collapsed,\n collapsible,\n isLink,\n isActiveLink,\n hasActiveLink,\n hasChildren,\n toggle\n };\n}\n"], - "mappings": ";;;;;;;;;;;AAAA,OAAO;;;ACAP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO,aAAa;AACpB,OAAO,YAAY;AACnB,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA2B;AACpC,SAAoB,WAAXA,gBAAqC;AAC9C,SAAoB,WAAXA,gBAA6B;AACtC,SAAoB,WAAXA,gBAAgC;AACzC,SAAoB,WAAXA,gBAAiC;AAC1C,SAAoB,WAAXA,gBAA6B;AACtC,SAAoB,WAAXA,gBAAiC;AAC1C,SAAoB,WAAXA,iBAA0B;AACnC,SAAoB,WAAXA,iBAAyB;AAClC,SAAoB,WAAXA,iBAAiC;AAC1C,SAAoB,WAAXA,iBAA+B;AACxC,SAAoB,WAAXA,iBAAgC;AACzC,SAAoB,WAAXA,iBAA6B;AACtC,SAAoB,WAAXA,iBAAgC;AACzC,SAAoB,WAAXA,iBAA6B;AACtC,SAAoB,WAAXA,iBAAoC;AAC7C,SAAoB,WAAXA,iBAAkC;;;AC5B3C,SAAS,wBAAwB;;;ACAjC,SAAS,uBAAuB;;;ACAhC,SAAS,gBAAgB;;;ACAzB,SAAS,WAAW,gBAAgB;AAC7B,IAAM,UAAU;;;ADgBhB,SAAS,oBAAoB,MAAM;AACtC,SAAO,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AACjD;;;AEXO,SAAS,WAAW,UAAU,MAAM;AACvC,MAAI,MAAM,QAAQ,QAAQ;AACtB,WAAO,QAAQ,QAAQ;AAC3B,MAAI,YAAY;AACZ,WAAO,CAAC;AACZ,SAAO,oBAAoB,IAAI;AAC/B,QAAM,MAAM,OAAO,KAAK,QAAQ,EAC3B,KAAK,CAAC,GAAG,MAAM;AAChB,WAAO,EAAE,MAAM,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,EAAE;AAAA,EAC9C,CAAC,EACI,KAAK,CAACC,SAAQ;AAEf,WAAO,KAAK,WAAW,oBAAoBA,IAAG,CAAC;AAAA,EACnD,CAAC;AACD,QAAM,UAAU,MAAM,SAAS,GAAG,IAAI,CAAC;AACvC,SAAO,MAAM,QAAQ,OAAO,IACtB,QAAQ,OAAO,IACf,QAAQ,QAAQ,OAAO,QAAQ,IAAI;AAC7C;AAIO,SAAS,iBAAiB,SAAS;AACtC,QAAM,SAAS,CAAC;AAChB,MAAI,iBAAiB;AACrB,aAAW,SAAS,SAAS;AACzB,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,KAAK,OAAO;AACZ,uBAAiB,OAAO,KAAK,IAAI;AACjC;AAAA,IACJ;AACA,QAAI,CAAC,OAAO,cAAc,GAAG;AACzB,aAAO,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,IAC7B;AACA,WAAO,cAAc,EAAE,MAAM,KAAK,IAAI;AAAA,EAC1C;AACA,SAAO;AACX;AAiCA,SAAS,QAAQ,OAAO,OAAO;AAC3B,SAAO,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,UAAU;AAC7B,UAAM,OAAO,EAAE,GAAG,MAAM;AACxB,UAAM,OAAO,KAAK,QAAQ;AAC1B,QAAI,QAAQ,KAAK;AACb,WAAK,OAAO,OAAO,KAAK;AAC5B,QAAI,KAAK;AACL,WAAK,QAAQ,QAAQ,KAAK,OAAO,IAAI;AACzC,WAAO;AAAA,EACX,CAAC;AACL;;;ACnFO,SAAS,aAAa;AACzB,QAAM,EAAE,aAAa,MAAM,OAAAC,OAAM,IAAI,QAAQ;AAC7C,QAAM,QAAQ,cAAc,oBAAoB;AAChD,QAAM,SAAS,IAAI,KAAK;AACxB,QAAM,WAAW,SAAS,MAAM;AAC5B,UAAM,gBAAgBA,OAAM,MAAM;AAClC,UAAM,eAAe,KAAK,MAAM;AAChC,WAAO,gBAAgB,WAAW,eAAe,YAAY,IAAI,CAAC;AAAA,EACtE,CAAC;AACD,QAAM,UAAU,IAAI,SAAS,KAAK;AAClC,QAAM,UAAU,CAAC,MAAM,SAAS;AAC5B,QAAI,KAAK,UAAU,IAAI,MAAM,KAAK,UAAU,IAAI;AAC5C,cAAQ,QAAQ,SAAS;AAAA,EACjC,CAAC;AACD,QAAM,aAAa,SAAS,MAAM;AAC9B,WAAQ,YAAY,MAAM,YAAY,SAClC,QAAQ,MAAM,SAAS,KACvB,YAAY,MAAM,WAAW;AAAA,EACrC,CAAC;AACD,QAAM,YAAY,SAAS,MAAM;AAC7B,QAAI;AACA,aAAO,YAAY,MAAM,SAAS,OAC5BA,OAAM,MAAM,UAAU,SACtB,YAAY,MAAM,UAAU;AACtC,WAAO;AAAA,EACX,CAAC;AACD,QAAM,WAAW,SAAS,MAAM;AAC5B,QAAI,YAAY,MAAM,WAAW;AAC7B,aAAO;AACX,QAAI,YAAY,MAAM,SAAS;AAC3B,aAAO,CAAC,CAAC,YAAY,MAAM;AAC/B,WAAOA,OAAM,MAAM,UAAU;AAAA,EACjC,CAAC;AACD,QAAM,mBAAmB,SAAS,MAAM,WAAW,SAAS,MAAM,KAAK;AACvE,QAAM,gBAAgB,SAAS,MAAM;AACjC,WAAO,WAAW,QAAQ,iBAAiB,QAAQ,KAAK,IAAI,CAAC;AAAA,EACjE,CAAC;AACD,WAAS,OAAO;AACZ,WAAO,QAAQ;AAAA,EACnB;AACA,WAAS,QAAQ;AACb,WAAO,QAAQ;AAAA,EACnB;AACA,WAAS,SAAS;AACd,WAAO,QAAQ,MAAM,IAAI,KAAK;AAAA,EAClC;AACA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ;AACJ;;;AJ3DA,IAAM,WAAW;AAEjB,IAAM,kBAAkB,CAAC;AAQlB,SAAS,WAAW,OAAO;AAC9B,QAAM,UAAU;AAAA,IACZ,GAAG,SAAS,iBAAiB,kCAAkC;AAAA,EACnE,EACK,OAAO,CAAC,OAAO,GAAG,MAAM,GAAG,cAAc,CAAC,EAC1C,IAAI,CAAC,OAAO;AACb,UAAM,QAAQ,OAAO,GAAG,QAAQ,CAAC,CAAC;AAClC,WAAO;AAAA,MACH,SAAS;AAAA,MACT,OAAO,gBAAgB,EAAE;AAAA,MACzB,MAAM,MAAM,GAAG;AAAA,MACf;AAAA,IACJ;AAAA,EACJ,CAAC;AACD,SAAO,eAAe,SAAS,KAAK;AACxC;AACA,SAAS,gBAAgB,GAAG;AACxB,MAAI,MAAM;AACV,aAAW,QAAQ,EAAE,YAAY;AAC7B,QAAI,KAAK,aAAa,GAAG;AACrB,UAAI,SAAS,KAAK,KAAK,SAAS;AAC5B;AACJ,aAAO,KAAK;AAAA,IAChB,WACS,KAAK,aAAa,GAAG;AAC1B,aAAO,KAAK;AAAA,IAChB;AAAA,EACJ;AACA,SAAO,IAAI,KAAK;AACpB;AACO,SAAS,eAAe,SAAS,OAAO;AAC3C,MAAI,UAAU,OAAO;AACjB,WAAO,CAAC;AAAA,EACZ;AACA,QAAM,eAAe,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAChE,MAAM,QACN,UAAU;AAChB,QAAM,CAAC,MAAM,GAAG,IAAI,OAAO,gBAAgB,WACrC,CAAC,aAAa,WAAW,IACzB,gBAAgB,SACZ,CAAC,GAAG,CAAC,IACL;AACV,SAAO,UAAU,SAAS,MAAM,GAAG;AACvC;AA8FA,SAAS,UAAU,MAAM,KAAK,KAAK;AAC/B,kBAAgB,SAAS;AACzB,QAAM,SAAS,CAAC;AAChB,QAAM,QAAQ,CAAC;AACf,OAAK,QAAQ,CAAC,SAAS;AACnB,UAAM,OAAO,EAAE,GAAG,MAAM,UAAU,CAAC,EAAE;AACrC,QAAI,SAAS,MAAM,MAAM,SAAS,CAAC;AACnC,WAAO,UAAU,OAAO,SAAS,KAAK,OAAO;AACzC,YAAM,IAAI;AACV,eAAS,MAAM,MAAM,SAAS,CAAC;AAAA,IACnC;AACA,QAAI,KAAK,QAAQ,UAAU,SAAS,eAAe,KAC9C,UAAU,kBAAkB,QAAS;AACtC,YAAM,KAAK,EAAE,OAAO,KAAK,OAAO,cAAc,KAAK,CAAC;AACpD;AAAA,IACJ;AACA,QAAI,KAAK,QAAQ,OAAO,KAAK,QAAQ;AACjC;AACJ,oBAAgB,KAAK,EAAE,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK,CAAC;AAC/D,QAAI;AACA,aAAO,SAAS,KAAK,IAAI;AAAA;AAEzB,aAAO,KAAK,IAAI;AACpB,UAAM,KAAK,IAAI;AAAA,EACnB,CAAC;AACD,SAAO;AACX;;;AD7KO,SAAS,cAAc;AAC1B,QAAM,EAAE,OAAAC,QAAO,YAAY,IAAI,QAAQ;AACvC,QAAM,UAAU,WAAW,CAAC,CAAC;AAC7B,QAAM,cAAc,SAAS,MAAM;AAC/B,WAAO,QAAQ,MAAM,SAAS;AAAA,EAClC,CAAC;AACD,mBAAiB,MAAM;AACnB,YAAQ,QAAQ,WAAW,YAAY,MAAM,WAAWA,OAAM,MAAM,OAAO;AAAA,EAC/E,CAAC;AACD,SAAO;AAAA,IACH;AAAA,IACA;AAAA,EACJ;AACJ;;;ADcA,IAAM,QAAQ;AAAA,EACV;AAAA,EACA,YAAY,CAAC,EAAE,IAAI,MAAM;AACrB,QAAI,UAAU,SAAS,OAAO;AAAA,EAClC;AACJ;AACA,IAAO,wBAAQ;", - "names": ["default", "dir", "theme", "theme"] -} diff --git a/docs/.vitepress/cache/deps/_metadata.json b/docs/.vitepress/cache/deps/_metadata.json index 7dffeeb..02bdaba 100644 --- a/docs/.vitepress/cache/deps/_metadata.json +++ b/docs/.vitepress/cache/deps/_metadata.json @@ -1,49 +1,43 @@ { - "hash": "d07f6dee", - "configHash": "dee2d3bc", + "hash": "8f855eaf", + "configHash": "1b3ca22f", "lockfileHash": "bd28b2c2", - "browserHash": "b96651ac", + "browserHash": "29e84937", "optimized": { "vue": { "src": "../../../node_modules/.pnpm/vue@3.5.13/node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "bc5c7f34", + "fileHash": "3215885f", "needsInterop": false }, "vitepress > @vue/devtools-api": { "src": "../../../node_modules/.pnpm/@vue+devtools-api@7.7.1/node_modules/@vue/devtools-api/dist/index.js", "file": "vitepress___@vue_devtools-api.js", - "fileHash": "00859881", + "fileHash": "5a5f95ef", "needsInterop": false }, "vitepress > @vueuse/core": { "src": "../../../node_modules/.pnpm/@vueuse+core@12.5.0/node_modules/@vueuse/core/index.mjs", "file": "vitepress___@vueuse_core.js", - "fileHash": "1c6da938", + "fileHash": "0fbf66f0", "needsInterop": false }, "vitepress > @vueuse/integrations/useFocusTrap": { "src": "../../../node_modules/.pnpm/@vueuse+integrations@12.5.0_focus-trap@7.6.4/node_modules/@vueuse/integrations/useFocusTrap.mjs", "file": "vitepress___@vueuse_integrations_useFocusTrap.js", - "fileHash": "11c08fcf", + "fileHash": "91b03896", "needsInterop": false }, "vitepress > mark.js/src/vanilla.js": { "src": "../../../node_modules/.pnpm/mark.js@8.11.1/node_modules/mark.js/src/vanilla.js", "file": "vitepress___mark__js_src_vanilla__js.js", - "fileHash": "b93dbfe7", + "fileHash": "99e4f81c", "needsInterop": false }, "vitepress > minisearch": { "src": "../../../node_modules/.pnpm/minisearch@7.1.1/node_modules/minisearch/dist/es/index.js", "file": "vitepress___minisearch.js", - "fileHash": "bfb6c3ac", - "needsInterop": false - }, - "@theme/index": { - "src": "../../../node_modules/.pnpm/vitepress@1.6.3_@algolia+client-search@5.20.2_postcss@8.5.2_search-insights@2.17.3/node_modules/vitepress/dist/client/theme-default/index.js", - "file": "@theme_index.js", - "fileHash": "07512287", + "fileHash": "cc176c9c", "needsInterop": false } }, diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 0000000..22945dd --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,473 @@ + + :root { + /* --- 核心色板:清晨薄雾 (Teal -> Sky) --- */ + --vp-c-brand-1: #14b8a6; /* Teal 500 */ + --vp-c-brand-2: #0ea5e9; /* Sky 500 */ + --vp-c-brand-3: #0f766e; /* Teal 700 */ + --vp-c-brand-soft: rgba(20, 184, 166, 0.14); + + /* --- 背景色:极简与通透 --- */ + --vp-c-bg: #ffffff; + --vp-c-bg-alt: #f8fafc; /* Slate 50 */ + --vp-c-bg-elv: #ffffff; + + /* --- 文字颜色:柔和的深灰,而非纯黑 --- */ + --vp-c-text-1: #334155; /* Slate 700 */ + --vp-c-text-2: #64748b; /* Slate 500 */ + --vp-c-text-3: #94a3b8; /* Slate 400 */ + + /* --- 边框与分隔 --- */ + --vp-c-border: #e2e8f0; + --vp-c-divider: #f1f5f9; + + /* --- 阴影:扩散柔光 --- */ + --vp-shadow-1: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); + --vp-shadow-2: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.025); + --vp-shadow-brand: 0 4px 14px 0 rgba(20, 184, 166, 0.3); + + /* --- 布局参数 --- */ + --vp-nav-height: 64px; + --vp-sidebar-width: 280px; + --vp-radius-default: 12px; + --vp-radius-small: 6px; + } + + /* * ========================================== + * 暗黑模式重构 (Deep Ocean Theme) + * ========================================== + */ + .dark { + /* 背景:更深邃的蓝黑色,更有沉浸感 */ + --vp-c-bg: #020617; /* Slate 950 */ + --vp-c-bg-alt: #0f172a; /* Slate 900 */ + --vp-c-bg-elv: #1e293b; /* Slate 800 */ + + /* 文字:降低纯白刺眼感,提高舒适度 */ + --vp-c-text-1: #f8fafc; /* Slate 50 */ + --vp-c-text-2: #94a3b8; /* Slate 400 */ + --vp-c-text-3: #64748b; /* Slate 500 */ + + /* 边框:更细腻的深色线条 */ + --vp-c-border: #1e293b; + --vp-c-divider: #0f172a; + + /* 品牌色:调亮,产生荧光感 */ + --vp-c-brand-1: #2dd4bf; /* Teal 400 */ + --vp-c-brand-2: #38bdf8; /* Sky 400 */ + --vp-c-brand-3: #14b8a6; /* Teal 500 */ + --vp-c-brand-soft: rgba(45, 212, 191, 0.15); /* 淡淡的荧光底色 */ + + /* 阴影:暗色环境下的微光 */ + --vp-shadow-1: 0 4px 6px -1px rgba(0, 0, 0, 0.5); + --vp-shadow-2: 0 10px 15px -3px rgba(0, 0, 0, 0.5); + --vp-shadow-brand: 0 0 20px rgba(45, 212, 191, 0.3); /* 品牌色发光 */ + } + + /* 全局基础设置 */ + body { + font-family: 'Noto Sans SC', sans-serif; + background-color: var(--vp-c-bg-alt); + color: var(--vp-c-text-1); + margin: 0; + transition: background-color 0.3s, color 0.3s; + overflow-x: hidden; + } + + a { text-decoration: none; color: inherit; transition: color 0.2s; } + + /* ------------------------------------------- + 布局结构 (模拟 VitePress 布局) + ------------------------------------------- */ + .app-container { + display: flex; + flex-direction: column; + min-height: 100vh; + } + + /* 1. 导航栏:毛玻璃 + 悬浮感 */ + .VPNavBar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--vp-nav-height); + background-color: rgba(255, 255, 255, 0.85); /* 稍微增加不透明度 */ + backdrop-filter: blur(20px) saturate(180%); + -webkit-backdrop-filter: blur(20px) saturate(180%); + border-bottom: 1px solid rgba(226, 232, 240, 0.6); + z-index: 50; + display: flex; + align-items: center; + padding: 0 24px; + justify-content: space-between; + } + .dark .VPNavBar { + background-color: rgba(2, 6, 23, 0.85); /* 深色背景半透 */ + border-bottom: 1px solid rgba(30, 41, 59, 0.7); + } + + .nav-logo { + font-weight: 700; + font-size: 1.2rem; + background: linear-gradient(120deg, var(--vp-c-brand-1), var(--vp-c-brand-2)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: flex; + align-items: center; + gap: 8px; + } + + .nav-links { + display: flex; + gap: 24px; + font-size: 0.9rem; + font-weight: 500; + color: var(--vp-c-text-2); + } + + .nav-link:hover { + color: var(--vp-c-brand-1); + } + + .nav-link.active { + color: var(--vp-c-brand-1); + position: relative; + } + .nav-link.active::after { + content: ''; + position: absolute; + bottom: -22px; + left: 0; + width: 100%; + height: 2px; + background: var(--vp-c-brand-1); + border-radius: 2px 2px 0 0; + box-shadow: 0 -2px 10px var(--vp-c-brand-1); /* 激活状态发光 */ + } + + /* 2. 侧边栏:极简卡片式 */ + .VPSidebar { + position: fixed; + top: var(--vp-nav-height); + left: 0; + bottom: 0; + width: var(--vp-sidebar-width); + background-color: var(--vp-c-bg); /* 实色背景 */ + border-right: 1px solid var(--vp-c-border); + padding: 32px 24px; + overflow-y: auto; + transform: translateX(0); + transition: transform 0.3s ease; + z-index: 40; + } + .dark .VPSidebar { + background-color: var(--vp-c-bg-alt); /* 侧边栏比内容区稍微亮一点点/或者暗一点 */ + } + + @media (max-width: 960px) { + .VPSidebar { transform: translateX(-100%); } + .VPSidebar.open { transform: translateX(0); } + } + + .sidebar-group-title { + font-weight: 700; + color: var(--vp-c-text-1); + margin-bottom: 12px; + margin-top: 24px; + font-size: 1rem; + } + .sidebar-group-title:first-child { margin-top: 0; } + + .sidebar-link { + display: block; + padding: 8px 12px; + margin: 4px 0; + border-radius: var(--vp-radius-small); + color: var(--vp-c-text-2); + font-size: 0.95rem; + transition: all 0.2s; + } + + .sidebar-link:hover { + background-color: var(--vp-c-bg-alt); + color: var(--vp-c-brand-1); + } + .dark .sidebar-link:hover { + background-color: var(--vp-c-bg-elv); + } + + .sidebar-link.active { + background-color: var(--vp-c-brand-soft); + color: var(--vp-c-brand-3); + font-weight: 600; + } + .dark .sidebar-link.active { + color: var(--vp-c-brand-1); + background-color: rgba(45, 212, 191, 0.1); /* 暗色模式下更通透 */ + box-shadow: inset 2px 0 0 0 var(--vp-c-brand-1); /* 左侧高亮条 */ + } + + /* 3. 主内容区:居中与留白 */ + .VPContent { + margin-top: var(--vp-nav-height); + margin-left: var(--vp-sidebar-width); + padding: 48px 64px; + min-height: calc(100vh - var(--vp-nav-height)); + background-color: var(--vp-c-bg); + transition: margin-left 0.3s ease; + } + + @media (max-width: 960px) { + .VPContent { margin-left: 0; padding: 32px 24px; } + } + + /* 内容排版 (Typography) */ + .vp-doc h1 { + font-size: 2.5rem; + font-weight: 800; + margin-bottom: 1.5rem; + line-height: 1.2; + background: linear-gradient(120deg, var(--vp-c-brand-1), var(--vp-c-brand-2)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: inline-block; + /* 暗色模式下加一点文字阴影增强可读性 */ + filter: drop-shadow(0 0 2em rgba(20, 184, 166, 0.2)); + } + .dark .vp-doc h1 { + filter: drop-shadow(0 0 25px rgba(45, 212, 191, 0.2)); + } + + .vp-doc h2 { + font-size: 1.8rem; + margin-top: 3rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--vp-c-border); + color: var(--vp-c-text-1); + } + + .vp-doc h3 { + font-size: 1.4rem; + margin-top: 2rem; + margin-bottom: 0.8rem; + color: var(--vp-c-text-1); + } + + .vp-doc p { + line-height: 1.8; + margin-bottom: 1.2rem; + color: var(--vp-c-text-2); + font-size: 1.05rem; + } + + /* 代码块美化 */ + .vp-doc pre { + background-color: #1e293b; /* Dark Slate */ + border-radius: var(--vp-radius-default); + padding: 20px; + overflow-x: auto; + color: #f8fafc; + margin: 24px 0; + border: 1px solid rgba(0,0,0,0.1); + box-shadow: var(--vp-shadow-1); + } + .dark .vp-doc pre { + background-color: #0f172a; /* 更深一层的背景 */ + border: 1px solid #1e293b; + } + + .vp-doc code { + font-family: 'Menlo', 'Monaco', 'Courier New', monospace; + font-size: 0.9em; + } + .vp-doc p code { + background-color: var(--vp-c-brand-soft); + color: var(--vp-c-brand-3); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.9em; + } + .dark .vp-doc p code { + color: var(--vp-c-brand-1); + background-color: rgba(45, 212, 191, 0.1); + } + + /* 引用块 (Blockquote) */ + .vp-doc blockquote { + margin: 1.5rem 0; + padding: 16px 24px; + background-color: var(--vp-c-bg-alt); + border-left: 4px solid var(--vp-c-brand-1); + border-radius: 0 var(--vp-radius-default) var(--vp-radius-default) 0; + color: var(--vp-c-text-2); + } + .dark .vp-doc blockquote { + background-color: rgba(30, 41, 59, 0.5); + } + + /* 按钮与卡片 (Features) */ + .vp-button { + display: inline-block; + padding: 0 24px; + height: 48px; + line-height: 48px; + border-radius: 24px; /* Pill shape */ + font-weight: 600; + font-size: 1rem; + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + cursor: pointer; + border: none; + } + + .vp-button.brand { + background: linear-gradient(135deg, var(--vp-c-brand-1), var(--vp-c-brand-2)); + color: white; + box-shadow: var(--vp-shadow-brand); + } + .dark .vp-button.brand { + color: #020617; /* 暗色模式下按钮文字用深色,因为背景亮 */ + } + + .vp-button.brand:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px -4px rgba(20, 184, 166, 0.5); + } + .dark .vp-button.brand:hover { + box-shadow: 0 0 30px -4px rgba(45, 212, 191, 0.6); /* 发光增强 */ + } + + .vp-button.alt { + background-color: var(--vp-c-bg-alt); + color: var(--vp-c-text-1); + border: 1px solid var(--vp-c-border); + } + .dark .vp-button.alt { + background-color: transparent; + border-color: var(--vp-c-text-2); + color: var(--vp-c-text-1); + } + + .vp-button.alt:hover { + border-color: var(--vp-c-brand-1); + color: var(--vp-c-brand-1); + } + + /* 工具类 */ + .theme-toggle { + background: none; + border: none; + cursor: pointer; + font-size: 1.5rem; + color: var(--vp-c-text-2); + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + border-radius: 50%; + transition: background 0.2s; + } + .theme-toggle:hover { + background-color: var(--vp-c-bg-alt); + color: var(--vp-c-brand-1); + } + .dark .theme-toggle:hover { + background-color: var(--vp-c-bg-elv); + } + + .mobile-menu-btn { + display: none; + background: none; + border: none; + font-size: 1.5rem; + color: var(--vp-c-text-1); + cursor: pointer; + } + + @media (max-width: 960px) { + .mobile-menu-btn { display: block; } + .nav-links { display: none; } + } + + /* 首页 Hero 样式模拟 */ + .hero-section { + text-align: center; + padding: 80px 20px; + max-width: 800px; + margin: 0 auto; + } + .hero-title { + font-size: 3.5rem; + font-weight: 900; + line-height: 1.1; + margin-bottom: 24px; + } + .hero-title span { + background: linear-gradient(120deg, var(--vp-c-brand-1), var(--vp-c-brand-2)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + .hero-desc { + font-size: 1.5rem; + color: var(--vp-c-text-2); + margin-bottom: 40px; + } + .hero-actions { + display: flex; + gap: 16px; + justify-content: center; + } + + /* Features Grid */ + .features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 24px; + margin-top: 64px; + } + .feature-card { + background-color: var(--vp-c-bg-alt); + padding: 24px; + border-radius: var(--vp-radius-default); + transition: transform 0.3s, box-shadow 0.3s; + border: 1px solid transparent; + } + .dark .feature-card { + background-color: rgba(30, 41, 59, 0.3); /* 微微透明 */ + border: 1px solid rgba(255,255,255,0.05); + } + + .feature-card:hover { + background-color: var(--vp-c-bg); + transform: translateY(-4px); + box-shadow: var(--vp-shadow-2); + border-color: var(--vp-c-brand-soft); + } + .dark .feature-card:hover { + background-color: var(--vp-c-bg-elv); + border-color: var(--vp-c-brand-1); + box-shadow: 0 0 15px rgba(45, 212, 191, 0.1); + } + + .feature-icon { + font-size: 2rem; + margin-bottom: 16px; + color: var(--vp-c-brand-1); + background-color: var(--vp-c-brand-soft); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + } + .feature-title { + font-weight: 700; + margin-bottom: 8px; + color: var(--vp-c-text-1); + } + .feature-details { + color: var(--vp-c-text-2); + font-size: 0.95rem; + line-height: 1.6; + } diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..db4242b --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,6 @@ +import DefaultTheme from 'vitepress/theme' +import './custom.css' + +export default { + ...DefaultTheme, +} diff --git a/docs/en/guide/configuration.md b/docs/en/guide/configuration.md index e69de29..89f6be3 100644 --- a/docs/en/guide/configuration.md +++ b/docs/en/guide/configuration.md @@ -0,0 +1,207 @@ +# Configuration Guide + +FileCodeBox provides rich configuration options that can be customized through the admin panel or by directly modifying the configuration. This document details all available configuration options. + +## Configuration Methods + +FileCodeBox supports two configuration methods: + +1. **Admin Panel Configuration** (Recommended): Access `/admin` to enter the admin panel and modify settings on the settings page +2. **Database Configuration**: Configuration is stored in the `data/filecodebox.db` database + +::: tip Note +On first startup, the system uses default configuration from `core/settings.py`. Modified configurations are saved to the database. +::: + +## Basic Settings + +### Site Information + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `name` | string | `文件快递柜 - FileCodeBox` | Site name, displayed in page title and navigation bar | +| `description` | string | `开箱即用的文件快传系统` | Site description, used for SEO | +| `keywords` | string | `FileCodeBox, 文件快递柜...` | Site keywords, used for SEO | +| `port` | int | `12345` | Service listening port | + +### Notification Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `notify_title` | string | `系统通知` | Notification title | +| `notify_content` | string | Welcome message | Notification content, supports HTML | +| `page_explain` | string | Legal disclaimer | Footer explanation text | +| `robotsText` | string | `User-agent: *\nDisallow: /` | robots.txt content | + +## Upload Settings + +### File Upload Limits + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `openUpload` | int | `1` | Enable upload functionality (1=enabled, 0=disabled) | +| `uploadSize` | int | `10485760` | Maximum single file upload size (bytes), default 10MB | +| `enableChunk` | int | `0` | Enable chunked upload (1=enabled, 0=disabled) | + +::: warning Note +`uploadSize` is in bytes. 10MB = 10 * 1024 * 1024 = 10485760 bytes +::: + +### Upload Rate Limiting + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `uploadMinute` | int | `1` | Upload limit time window (minutes) | +| `uploadCount` | int | `10` | Maximum uploads allowed within the time window | + +Example: Default configuration allows up to 10 uploads per minute. + + +### File Expiration Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | Available expiration methods | +| `max_save_seconds` | int | `0` | Maximum file retention time (seconds), 0 means no limit | + +Expiration methods explained: +- `day` - Expire by days +- `hour` - Expire by hours +- `minute` - Expire by minutes +- `forever` - Never expire +- `count` - Expire by download count + +## Theme Settings + +### Theme Selection + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `themesSelect` | string | `themes/2024` | Currently active theme | +| `themesChoices` | list | See below | Available themes list | + +Default available themes: +```json +[ + { + "name": "2023", + "key": "themes/2023", + "author": "Lan", + "version": "1.0" + }, + { + "name": "2024", + "key": "themes/2024", + "author": "Lan", + "version": "1.0" + } +] +``` + +### Interface Style + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `opacity` | float | `0.9` | Interface opacity (0-1) | +| `background` | string | `""` | Custom background image URL, empty uses default background | + +## Admin Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `admin_token` | string | `FileCodeBox2023` | Admin login password | +| `showAdminAddr` | int | `0` | Show admin panel entry on homepage (1=show, 0=hide) | + +::: danger Security Warning +Always change the default `admin_token` in production environments! Using the default password poses serious security risks. +::: + +## Security Settings + +### Error Rate Limiting + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `errorMinute` | int | `1` | Error limit time window (minutes) | +| `errorCount` | int | `1` | Maximum errors allowed within the time window | + +This setting prevents brute-force attacks on extraction codes. + +## Storage Settings + +### Storage Type + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `file_storage` | string | `local` | Storage backend type | +| `storage_path` | string | `""` | Custom storage path | + +Supported storage types: +- `local` - Local storage +- `s3` - S3-compatible storage (AWS S3, Aliyun OSS, MinIO, etc.) +- `onedrive` - OneDrive storage +- `webdav` - WebDAV storage +- `opendal` - OpenDAL storage + +For detailed storage configuration, see [Storage Configuration](/en/guide/storage). + +## Configuration Examples + +### Example 1: Small Personal Use + +Suitable for personal or small team use with relaxed limits: + +```python +{ + "name": "My File Share", + "uploadSize": 52428800, # 50MB + "uploadMinute": 5, # 5 minutes + "uploadCount": 20, # Max 20 uploads + "expireStyle": ["day", "hour", "forever"], + "admin_token": "your-secure-password", + "showAdminAddr": 1 +} +``` + +### Example 2: Public Service + +Suitable for public services requiring stricter limits: + +```python +{ + "name": "Public File Box", + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, # 1 minute + "uploadCount": 5, # Max 5 uploads + "errorMinute": 5, # 5 minutes + "errorCount": 3, # Max 3 errors + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # Max retention 1 day + "admin_token": "very-secure-password-123", + "showAdminAddr": 0 +} +``` + +### Example 3: Enterprise Internal Use + +Suitable for enterprise internal use with large file and chunked upload support: + +```python +{ + "name": "Enterprise File Transfer", + "uploadSize": 1073741824, # 1GB + "enableChunk": 1, # Enable chunked upload + "uploadMinute": 10, # 10 minutes + "uploadCount": 100, # Max 100 uploads + "expireStyle": ["day", "forever"], + "file_storage": "s3", # Use S3 storage + "admin_token": "enterprise-secure-token", + "showAdminAddr": 1 +} +``` + +## Next Steps + +- [Storage Configuration](/en/guide/storage) - Learn how to configure different storage backends +- [Security Settings](/en/guide/security) - Learn how to enhance system security +- [File Sharing](/en/guide/share) - Learn about file sharing features diff --git a/docs/en/guide/management.md b/docs/en/guide/management.md index e69de29..e22a65c 100644 --- a/docs/en/guide/management.md +++ b/docs/en/guide/management.md @@ -0,0 +1,413 @@ +# Admin Panel + +FileCodeBox provides a fully-featured admin panel that allows administrators to conveniently manage files, view system status, and modify configurations. This document introduces the various features and usage of the admin panel. + +## Accessing the Admin Panel + +### Login Method + +The admin panel is located at the `/admin` path. Access method: + +1. Visit `http://your-domain.com/admin` in your browser +2. Enter the admin password (the value of the `admin_token` configuration) +3. Click the login button + +::: tip Tip +The default admin password is `FileCodeBox2023`. Be sure to change this password in production environments. See [Security Settings](/en/guide/security) for details. +::: + +### Show Admin Entry + +By default, the admin panel entry is not shown on the homepage. You can control whether to show it via configuration: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `showAdminAddr` | int | `0` | Show admin entry on homepage (1=show, 0=hide) | + +::: warning Security Recommendation +For public services, it's recommended to keep `showAdminAddr` at `0` and access the admin panel directly via the `/admin` path to reduce the risk of malicious scanning. +::: + +### Authentication Mechanism + +The admin panel uses JWT (JSON Web Token) for authentication: + +1. After successful login, the server returns a Token containing admin identity +2. Subsequent requests carry the Token via `Authorization: Bearer ` header +3. Token is used to verify admin identity, ensuring only authorized users can access admin functions + +## Dashboard + +After logging in, you first see the dashboard page, which displays the overall system status. + +### Statistics + +The dashboard displays the following key metrics: + +| Metric | Description | +|--------|-------------| +| **Total Files** (`totalFiles`) | Total number of files stored in the system | +| **Storage Used** (`storageUsed`) | Total storage space occupied by all files (bytes) | +| **System Uptime** (`sysUptime`) | Time when the system was first started | +| **Yesterday's Uploads** (`yesterdayCount`) | Number of files uploaded yesterday | +| **Yesterday's Upload Size** (`yesterdaySize`) | Total size of files uploaded yesterday (bytes) | +| **Today's Uploads** (`todayCount`) | Number of files uploaded today so far | +| **Today's Upload Size** (`todaySize`) | Total size of files uploaded today (bytes) | + +### Metric Notes + +- **Total Files**: Includes all unexpired files and text shares +- **Storage Used**: Shows actual storage space occupied by files, excluding database and other system files +- **Yesterday/Today Statistics**: Calculated based on file creation time, useful for understanding system usage trends + +::: tip Tip +Storage usage is displayed in bytes. For example, `10485760` represents approximately 10MB. +::: + +## File Management + +### File List + +The file management page displays all shared files in the system, supporting pagination and search. + +**List information includes:** +- File ID +- Extraction code (code) +- Filename prefix (prefix) +- File extension (suffix) +- File size +- Creation time +- Expiration time +- Remaining download count + +### Search Files + +Use the search function to quickly find specific files: + +1. Enter keywords in the search box +2. System performs fuzzy matching based on filename prefix (prefix) +3. Search results update in real-time + +**Search examples:** +- Enter `report` to find all files with "report" in the filename +- Enter `.pdf` to find all PDF files (if the filename contains this string) + +### Pagination + +The file list supports paginated display: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `page` | `1` | Current page number | +| `size` | `10` | Items per page | + + +### Delete Files + +Administrators can delete any file: + +1. Find the file to delete in the file list +2. Click the delete button +3. Confirm the delete operation + +::: danger Warning +Delete operations are irreversible! Files will be permanently deleted from the storage backend, and database records will also be removed. +::: + +**Delete process:** +1. System first deletes the actual file from the storage backend (local/S3/OneDrive, etc.) +2. Then deletes the file record from the database +3. After deletion, the corresponding extraction code becomes invalid + +### Download Files + +Administrators can directly download any file: + +1. Find the target file in the file list +2. Click the download button +3. File will be downloaded via browser + +For text shares, the system returns text content directly instead of downloading a file. + +### Modify File Information + +Administrators can modify some information of shared files: + +| Modifiable Field | Description | +|------------------|-------------| +| `code` | Extraction code (must be unique, cannot duplicate other files) | +| `prefix` | Filename prefix | +| `suffix` | File extension | +| `expired_at` | Expiration time | +| `expired_count` | Remaining download count | + +**Modify extraction code:** +``` +Original code: abc123 +New code: myfile2024 +``` + +::: warning Note +When modifying extraction codes, the system checks if the new code is already in use. If an identical extraction code exists, the modification will fail. +::: + +## Local File Management + +In addition to managing shared files, the admin panel also provides local file management functionality for managing files in the `data/local` directory. + +### View Local Files + +The local file list displays all files in the `data/local` directory: + +| Information | Description | +|-------------|-------------| +| Filename | Complete filename | +| Creation Time | File creation time | +| File Size | File size (bytes) | + +### Share Local Files + +You can quickly share local files: + +1. Select the file to share in the local file list +2. Set expiration method and value +3. Click the share button +4. System generates extraction code + +**Share parameters:** + +| Parameter | Description | +|-----------|-------------| +| `filename` | Filename to share | +| `expire_style` | Expiration method (day/hour/minute/forever/count) | +| `expire_value` | Expiration value (days/hours/minutes/download count) | + +### Delete Local Files + +You can delete files in the `data/local` directory: + +1. Find the file to delete in the local file list +2. Click the delete button +3. Confirm deletion + +::: tip Use Cases +Local file management is useful for: +- Sharing files after batch uploading to the server +- Managing files uploaded to the server through other means +- Cleaning up unnecessary local files +::: + +## System Settings + +### View Configuration + +On the system settings page, you can view the current values of all configuration items. Configuration items are grouped by category: + +- Basic settings (site name, description, etc.) +- Upload settings (file size limits, rate limits, etc.) +- Storage settings (storage type, path, etc.) +- Theme settings (theme selection, opacity, etc.) +- Security settings (admin password, error limits, etc.) + +### Modify Configuration + +Administrators can modify most configurations through the admin panel: + +1. Go to the system settings page +2. Find the configuration item to modify +3. Enter the new value +4. Click the save button + +**Modifiable configuration items:** + +| Category | Example Settings | +|----------|------------------| +| Basic Settings | `name`, `description`, `keywords`, `notify_title`, `notify_content` | +| Upload Settings | `uploadSize`, `uploadMinute`, `uploadCount`, `openUpload`, `enableChunk` | +| Expiration Settings | `expireStyle`, `max_save_seconds` | +| Theme Settings | `themesSelect`, `opacity`, `background` | +| Security Settings | `admin_token`, `showAdminAddr`, `errorMinute`, `errorCount` | +| Storage Settings | `file_storage`, `storage_path` and storage backend-specific configurations | + +::: warning Note +- `admin_token` (admin password) cannot be set to empty +- `themesChoices` (theme list) cannot be modified through the admin panel +- After modifying storage settings, existing files will not be automatically migrated +::: + +### Configuration Effect + +Configuration changes take effect immediately without restarting the service. Configurations are saved in the database and persist after restart. + +**Configuration storage location:** +- Database: `data/filecodebox.db` +- Table name: `keyvalue` +- Key name: `settings` + +## API Endpoints + +All admin panel functions are implemented through REST APIs. Here are the main endpoints: + +### Authentication Endpoint + +**Login** +``` +POST /admin/login +Content-Type: application/json + +{ + "password": "your-admin-password" +} +``` + +Response: +```json +{ + "code": 200, + "detail": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "Bearer" + } +} +``` + +### Dashboard Endpoint + +**Get Statistics** +``` +GET /admin/dashboard +Authorization: Bearer +``` + +### File Management Endpoints + +**Get File List** +``` +GET /admin/file/list?page=1&size=10&keyword= +Authorization: Bearer +``` + +**Delete File** +``` +DELETE /admin/file/delete +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123 +} +``` + +**Download File** +``` +GET /admin/file/download?id=123 +Authorization: Bearer +``` + +**Modify File Information** +``` +PATCH /admin/file/update +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123, + "code": "newcode", + "expired_at": "2024-12-31T23:59:59" +} +``` + +### Local File Endpoints + +**Get Local File List** +``` +GET /admin/local/lists +Authorization: Bearer +``` + +**Delete Local File** +``` +DELETE /admin/local/delete +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt" +} +``` + +**Share Local File** +``` +POST /admin/local/share +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt", + "expire_style": "day", + "expire_value": 7 +} +``` + +### Configuration Endpoints + +**Get Configuration** +``` +GET /admin/config/get +Authorization: Bearer +``` + +**Update Configuration** +``` +PATCH /admin/config/update +Authorization: Bearer +Content-Type: application/json + +{ + "admin_token": "new-password", + "uploadSize": 52428800 +} +``` + +## Common Issues + +### Forgot Admin Password + +If you forgot the admin password, you can reset it through the following methods: + +1. Stop the FileCodeBox service +2. Open `data/filecodebox.db` using an SQLite tool +3. Query the record with `key='settings'` in the `keyvalue` table +4. Modify the `admin_token` value in the JSON +5. Restart the service + +```sql +-- View current configuration +SELECT * FROM keyvalue WHERE key = 'settings'; + +-- Or delete configuration to restore default password +DELETE FROM keyvalue WHERE key = 'settings'; +``` + +### File Deletion Failed + +If an error occurs when deleting files, possible reasons: + +1. **Storage backend connection failed**: Check if storage configuration is correct +2. **File no longer exists**: File may have been manually deleted +3. **Insufficient permissions**: Check write permissions for storage directory + +### Configuration Changes Not Taking Effect + +If configuration changes don't take effect: + +1. Check if you clicked the save button +2. Refresh the page to see if configuration was saved +3. Check browser console for error messages +4. Confirm configuration value format is correct (e.g., don't enter strings for numeric types) + +## Next Steps + +- [Configuration Guide](/en/guide/configuration) - Learn detailed descriptions of all configuration options +- [Security Settings](/en/guide/security) - Learn how to enhance system security +- [Storage Configuration](/en/guide/storage) - Configure different storage backends diff --git a/docs/en/guide/security.md b/docs/en/guide/security.md index e69de29..3c74118 100644 --- a/docs/en/guide/security.md +++ b/docs/en/guide/security.md @@ -0,0 +1,325 @@ +# Security Settings + +FileCodeBox provides multiple layers of security mechanisms to protect your file sharing service. This document explains how to properly configure security options to ensure secure system operation. + +## Admin Password + +### Change Default Password + +::: danger Important Security Warning +FileCodeBox's default admin password is `FileCodeBox2023`. **You must change this password immediately in production environments!** Using the default password allows anyone to access your admin panel. +::: + +There are two ways to change the admin password: + +**Method 1: Via Admin Panel (Recommended)** + +1. Access `/admin` to enter the admin panel +2. Log in with the current password +3. Go to the "System Settings" page +4. Find the `admin_token` configuration item +5. Enter a new secure password and save + +**Method 2: Via Database** + +Configuration is stored in the `keyvalue` table of the `data/filecodebox.db` database. You can directly modify the `admin_token` value. + +### Password Security Recommendations + +- Use a strong password with at least 16 characters +- Include uppercase and lowercase letters, numbers, and special characters +- Avoid common words or personal information +- Change password regularly + +```python +# Recommended password format example +"admin_token": "Xk9#mP2$vL5@nQ8&wR3" +``` + +### Hide Admin Entry + +By default, the admin panel entry is hidden. You can control whether to show the admin entry on the homepage via the `showAdminAddr` configuration: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `showAdminAddr` | int | `0` | Show admin entry (1=show, 0=hide) | + +::: tip Recommendation +For public services, it's recommended to keep `showAdminAddr` at `0` and access the admin panel directly via the `/admin` path. +::: + +## IP Rate Limiting + +FileCodeBox has built-in IP-based rate limiting mechanisms to effectively prevent abuse and attacks. + +### Upload Rate Limiting + +Limit the number of uploads from a single IP within a specified time: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `uploadMinute` | int | `1` | Upload limit time window (minutes) | +| `uploadCount` | int | `10` | Maximum uploads allowed within the time window | + +**How it works:** +- System records upload requests from each IP +- When an IP's upload count reaches `uploadCount` within `uploadMinute` minutes +- Subsequent upload requests from that IP will be rejected with HTTP 423 error +- Counter resets after the time window expires + +**Configuration examples:** + +```python +# Relaxed configuration: Max 20 uploads in 5 minutes +{ + "uploadMinute": 5, + "uploadCount": 20 +} + +# Strict configuration: Max 3 uploads in 1 minute +{ + "uploadMinute": 1, + "uploadCount": 3 +} +``` + + +### Error Rate Limiting + +Limit the number of error attempts from a single IP to prevent brute-force attacks on extraction codes: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `errorMinute` | int | `1` | Error limit time window (minutes) | +| `errorCount` | int | `1` | Maximum errors allowed within the time window | + +**How it works:** +- When a user enters an incorrect extraction code, the system records the error count for that IP +- When error count reaches `errorCount`, that IP will be temporarily locked +- Lock duration is `errorMinute` minutes +- During lockout, all extraction requests from that IP will be rejected + +**Configuration example:** + +```python +# Anti-brute-force configuration: Max 3 errors in 5 minutes +{ + "errorMinute": 5, + "errorCount": 3 +} +``` + +::: warning Note +The default configuration `errorMinute=1, errorCount=1` is very strict, meaning you need to wait 1 minute after entering one incorrect extraction code before retrying. Adjust this configuration based on actual needs. +::: + +## Upload Restrictions + +### File Size Limit + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `uploadSize` | int | `10485760` | Maximum single file upload size (bytes), default 10MB | +| `openUpload` | int | `1` | Enable upload functionality (1=enabled, 0=disabled) | + +**Common size conversions:** +- 10MB = 10 * 1024 * 1024 = `10485760` +- 50MB = 50 * 1024 * 1024 = `52428800` +- 100MB = 100 * 1024 * 1024 = `104857600` +- 1GB = 1024 * 1024 * 1024 = `1073741824` + +### File Expiration Settings + +Through file expiration mechanisms, you can automatically clean up expired files, reducing storage usage and security risks: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | Available expiration methods | +| `max_save_seconds` | int | `0` | Maximum file retention time (seconds), 0 means no limit | + +**Expiration methods explained:** +- `day` - Expire by days +- `hour` - Expire by hours +- `minute` - Expire by minutes +- `forever` - Never expire (requires alphanumeric extraction code) +- `count` - Expire by download count + +**Security recommendations:** + +For public services, it's recommended to: +1. Remove the `forever` option to avoid permanent file storage +2. Set `max_save_seconds` to limit maximum retention time +3. Prefer using `count` method for automatic deletion after download + +```python +# Recommended configuration for public services +{ + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400 # Max retention 1 day +} +``` + +### Disable Upload Functionality + +In some cases, you may need to temporarily disable upload functionality: + +```python +{ + "openUpload": 0 # Disable upload functionality +} +``` + +## Reverse Proxy Security Configuration + +In production environments, Nginx or other reverse proxy servers are typically used. Here are security configuration recommendations: + +### Nginx Configuration Example + +```nginx +server { + listen 80; + server_name your-domain.com; + + # Force HTTPS redirect + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + # SSL certificate configuration + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers on; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Limit request body size (match uploadSize configuration) + client_max_body_size 100M; + + # Pass real IP + location / { + proxy_pass http://127.0.0.1:12345; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Static resource caching + location /assets { + proxy_pass http://127.0.0.1:12345; + proxy_cache_valid 200 7d; + add_header Cache-Control "public, max-age=604800"; + } +} +``` + +### Key Security Configuration Notes + +**1. Pass Real IP** + +FileCodeBox's IP limiting functionality depends on obtaining the client's real IP. The system obtains IP in the following order: +1. `X-Real-IP` request header +2. `X-Forwarded-For` request header +3. Direct client connection IP + +Ensure the reverse proxy correctly sets these headers: + +```nginx +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + +**2. Request Body Size Limit** + +Nginx's `client_max_body_size` should match or be slightly larger than FileCodeBox's `uploadSize` configuration: + +```nginx +client_max_body_size 100M; # Allow max 100MB uploads +``` + +**3. HTTPS Encryption** + +It's strongly recommended to enable HTTPS in production environments: +- Protect uploaded file content +- Protect admin login credentials +- Prevent man-in-the-middle attacks + +### Caddy Configuration Example + +```nginx +your-domain.com { + reverse_proxy localhost:12345 + + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Strict-Transport-Security "max-age=31536000; includeSubDomains" + } +} +``` + +## Security Checklist + +Before deploying FileCodeBox, confirm the following security configurations: + +- [ ] Changed default admin password `admin_token` +- [ ] Hidden admin entry `showAdminAddr: 0` +- [ ] Configured appropriate upload rate limiting +- [ ] Configured error rate limiting to prevent brute-force attacks +- [ ] Set reasonable file size limits +- [ ] Configured file expiration policy +- [ ] Enabled HTTPS encryption +- [ ] Reverse proxy correctly passes real IP +- [ ] Set security response headers + +## Recommended Security Configurations + +### Public Service Configuration + +```python +{ + "admin_token": "your-very-secure-password", + "showAdminAddr": 0, + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, + "uploadCount": 5, + "errorMinute": 5, + "errorCount": 3, + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # Max 1 day + "openUpload": 1 +} +``` + +### Internal Service Configuration + +```python +{ + "admin_token": "internal-secure-password", + "showAdminAddr": 1, + "uploadSize": 104857600, # 100MB + "uploadMinute": 5, + "uploadCount": 50, + "errorMinute": 1, + "errorCount": 5, + "expireStyle": ["day", "hour", "forever"], + "max_save_seconds": 0, # No limit + "openUpload": 1 +} +``` + +## Next Steps + +- [Configuration Guide](/en/guide/configuration) - Learn about all configuration options +- [Storage Configuration](/en/guide/storage) - Configure secure storage backends +- [File Sharing](/en/guide/share) - Learn about file sharing features diff --git a/docs/en/guide/share.md b/docs/en/guide/share.md index e69de29..0abb1a2 100644 --- a/docs/en/guide/share.md +++ b/docs/en/guide/share.md @@ -0,0 +1,342 @@ +# File Sharing + +FileCodeBox provides simple and easy-to-use file and text sharing functionality. Users can securely share and retrieve files using extraction codes. + +## Sharing Methods + +FileCodeBox supports two sharing methods: + +1. **Text Sharing** - Share text content directly, suitable for code snippets, configuration files, etc. +2. **File Sharing** - Upload files for sharing, supports various file formats + +## Text Sharing + +### How to Use + +1. Select the "Text Share" tab on the homepage +2. Enter or paste the content to share in the text box +3. Select expiration method and time +4. Click the "Share" button +5. Get the extraction code + +### Text Size Limit + +::: warning Note +The maximum content size for text sharing is **222KB** (227,328 bytes). If content exceeds this limit, it's recommended to use file sharing instead. +::: + +Text content size is calculated using UTF-8 encoding. Chinese characters typically occupy 3 bytes. + +### API Endpoint + +**POST** `/share/text/` + +Request parameters: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `text` | string | Yes | Text content to share | +| `expire_value` | int | No | Expiration value, default 1 | +| `expire_style` | string | No | Expiration method, default `day` | + +Response example: + +```json +{ + "code": 200, + "detail": { + "code": "123456" + } +} +``` + +## File Sharing + +### How to Use + +1. Select the "File Share" tab on the homepage +2. Click the upload area or drag files to the upload area +3. Select expiration method and time +4. Click the "Upload" button +5. Get the extraction code + +### File Size Limit + +The default maximum single file upload size is **10MB**. Administrators can modify this limit via the `uploadSize` configuration. + +::: tip Tip +If you need to upload large files, contact the administrator to enable chunked upload functionality or adjust the `uploadSize` configuration. +::: + +### Supported Upload Methods + +- **Click Upload** - Click the upload area to select files +- **Drag Upload** - Drag files to the upload area +- **Paste Upload** - Paste images from clipboard (supported by some themes) + +### API Endpoint + +**POST** `/share/file/` + +Request parameters (multipart/form-data): + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | file | Yes | File to upload | +| `expire_value` | int | No | Expiration value, default 1 | +| `expire_style` | string | No | Expiration method, default `day` | + +Response example: + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + + +## Expiration Settings + +FileCodeBox supports multiple flexible expiration methods: + +| Expiration Method | Parameter Value | Description | +|-------------------|-----------------|-------------| +| By Days | `day` | File expires after specified days | +| By Hours | `hour` | File expires after specified hours | +| By Minutes | `minute` | File expires after specified minutes | +| Never Expire | `forever` | File is permanently valid | +| By Count | `count` | File expires after specified download count | + +::: info Note +- Administrators can control available expiration methods via the `expireStyle` configuration +- Administrators can limit maximum file retention time via the `max_save_seconds` configuration +::: + +### Expiration Method Examples + +```bash +# File expires after 3 days +expire_value=3, expire_style=day + +# File expires after 12 hours +expire_value=12, expire_style=hour + +# File expires after 30 minutes +expire_value=30, expire_style=minute + +# File never expires +expire_value=1, expire_style=forever + +# File expires after 5 downloads +expire_value=5, expire_style=count +``` + +## Retrieving Files + +### How to Use + +1. Enter the extraction code in the "Retrieve File" area on the homepage +2. Click the "Retrieve" button +3. System displays file information (filename, size, etc.) +4. Click the "Download" button to download the file, or view text content directly + +### Extraction Code Notes + +- Extraction codes are typically **6-digit numbers** +- Files that never expire use **alphanumeric** extraction codes +- Extraction codes are case-sensitive (for alphanumeric codes) + +### API Endpoints + +**Query File Information** + +**POST** `/share/select/` + +Request parameters: + +```json +{ + "code": "123456" +} +``` + +Response example (file): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "example.pdf", + "size": 1048576, + "text": "https://example.com/download/..." + } +} +``` + +Response example (text): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "Text", + "size": 1024, + "text": "This is the shared text content..." + } +} +``` + +**Direct File Download** + +**GET** `/share/select/?code=123456` + +This endpoint returns file content directly, suitable for direct browser access. + +## Chunked Upload (Large Files) + +For large file uploads, FileCodeBox supports chunked upload functionality. This feature requires administrator enablement (`enableChunk=1`). + +### Chunked Upload Flow + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + C->>S: 1. Initialize upload (POST /chunk/upload/init/) + S-->>C: Return upload_id and chunk info + + loop Each chunk + C->>S: 2. Upload chunk (POST /chunk/upload/chunk/{upload_id}/{chunk_index}) + S-->>C: Return chunk hash + end + + C->>S: 3. Complete upload (POST /chunk/upload/complete/{upload_id}) + S-->>C: Return extraction code +``` + +### 1. Initialize Upload + +**POST** `/chunk/upload/init/` + +Request parameters: + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "sha256_hash_of_file" +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file_name` | string | Yes | Filename | +| `file_size` | int | Yes | Total file size (bytes) | +| `chunk_size` | int | No | Chunk size, default 5MB | +| `file_hash` | string | Yes | SHA256 hash of the file | + +Response example: + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +### 2. Upload Chunk + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +- `upload_id` - Upload session ID returned during initialization +- `chunk_index` - Chunk index, starting from 0 + +Request body: Chunk file data (multipart/form-data) + +Response example: + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "sha256_hash_of_chunk" + } +} +``` + +### 3. Complete Upload + +**POST** `/chunk/upload/complete/{upload_id}` + +Request parameters: + +```json +{ + "expire_value": 1, + "expire_style": "day" +} +``` + +Response example: + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### Resume Upload + +Chunked upload supports resume functionality. If upload is interrupted: + +1. Call the initialization endpoint again with the same `file_hash` +2. Server returns `uploaded_chunks` list containing already uploaded chunk indices +3. Client only needs to upload chunks not in the list + +## Error Handling + +### Common Error Codes + +| Error Code | Description | Solution | +|------------|-------------|----------| +| 403 | File size exceeds limit | Reduce file size or contact administrator to adjust limit | +| 403 | Content too large | Text exceeds 222KB, use file sharing instead | +| 403 | Upload rate limit | Wait a while before retrying | +| 404 | File not found | Check if extraction code is correct | +| 404 | File expired | File has expired or download count exhausted | + +### Rate Limiting + +To prevent abuse, the system has rate limits on upload and retrieval operations: + +- **Upload limit**: Default max 10 uploads per minute +- **Error limit**: Default max 1 error attempt per minute + +::: tip Tip +If you encounter rate limiting, wait for the limit time window to pass before retrying. +::: + +## Next Steps + +- [Configuration Guide](/en/guide/configuration) - Learn how to configure sharing-related settings +- [Storage Configuration](/en/guide/storage) - Learn about file storage methods +- [Security Settings](/en/guide/security) - Learn about security-related configurations +- [Admin Panel](/en/guide/management) - Learn how to manage shared files diff --git a/docs/en/guide/storage.md b/docs/en/guide/storage.md index e69de29..6997e43 100644 --- a/docs/en/guide/storage.md +++ b/docs/en/guide/storage.md @@ -0,0 +1,395 @@ +# Storage Configuration + +FileCodeBox supports multiple storage backends. You can choose the appropriate storage method based on your needs. This document details the configuration methods for various storage backends. + +## Storage Types Overview + +| Storage Type | Config Value | Description | +|--------------|--------------|-------------| +| Local Storage | `local` | Default storage method, files saved on local server | +| S3-Compatible Storage | `s3` | Supports AWS S3, Aliyun OSS, MinIO, etc. | +| OneDrive | `onedrive` | Microsoft OneDrive cloud storage (work/school accounts only) | +| WebDAV | `webdav` | Storage services supporting WebDAV protocol | +| OpenDAL | `opendal` | Integrate more storage services via OpenDAL | + +## Local Storage + +Local storage is the default storage method. Files are saved in the server's `data/` directory. + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | `local` | Storage type | +| `storage_path` | string | `""` | Custom storage path (optional) | + +### Configuration Example + +```bash +file_storage=local +storage_path= +``` + +### Notes + +- Files are stored by default in `data/share/data/` directory +- Subdirectories are automatically created by date: `year/month/day/fileID/` +- In production, it's recommended to mount the `data/` directory to persistent storage + +## S3-Compatible Storage + +Supports all S3-compatible object storage services, including AWS S3, Aliyun OSS, MinIO, Tencent Cloud COS, etc. + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | - | Set to `s3` | +| `s3_access_key_id` | string | `""` | Access Key ID | +| `s3_secret_access_key` | string | `""` | Secret Access Key | +| `s3_bucket_name` | string | `""` | Bucket name | +| `s3_endpoint_url` | string | `""` | S3 endpoint URL | +| `s3_region_name` | string | `auto` | Region name | +| `s3_signature_version` | string | `s3v2` | Signature version (`s3v2` or `s3v4`) | +| `s3_hostname` | string | `""` | S3 hostname (alternative) | +| `s3_proxy` | int | `0` | Download through server proxy (1=yes, 0=no) | +| `aws_session_token` | string | `""` | AWS session token (optional) | + + +### AWS S3 Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=AKIAIOSFODNN7EXAMPLE +s3_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +s3_bucket_name=my-filecodebox-bucket +s3_endpoint_url=https://s3.amazonaws.com +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +### Aliyun OSS Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=YourAccessKeyId +s3_secret_access_key=YourSecretAccessKey +s3_bucket_name=bucket-name +s3_endpoint_url=https://bucket-name.oss-cn-hangzhou.aliyuncs.com +s3_region_name=oss-cn-hangzhou +s3_signature_version=s3v4 +``` + +::: tip Aliyun OSS Endpoint Format +Endpoint URL format: `https://..aliyuncs.com` + +Common regions: +- Hangzhou: `oss-cn-hangzhou` +- Shanghai: `oss-cn-shanghai` +- Beijing: `oss-cn-beijing` +- Shenzhen: `oss-cn-shenzhen` +::: + +### MinIO Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=minioadmin +s3_secret_access_key=minioadmin +s3_bucket_name=filecodebox +s3_endpoint_url=http://localhost:9000 +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +::: warning MinIO Notes +- `s3_endpoint_url` should be the MinIO API interface address +- `s3_region_name` should match the `Server Location` in MinIO configuration +- Ensure the bucket is created and has correct access permissions +::: + +### Tencent Cloud COS Configuration Example + +```bash +file_storage=s3 +s3_access_key_id=YourSecretId +s3_secret_access_key=YourSecretKey +s3_bucket_name=bucket-name-1250000000 +s3_endpoint_url=https://cos.ap-guangzhou.myqcloud.com +s3_region_name=ap-guangzhou +s3_signature_version=s3v4 +``` + +### Proxy Download + +When `s3_proxy=1`, file downloads are proxied through the server instead of directly from S3. This is useful when: + +- S3 bucket doesn't allow public access +- Need to hide the actual storage address +- Network environment restricts direct S3 access + +## OneDrive Storage + +OneDrive storage supports saving files to Microsoft OneDrive cloud storage. + +::: warning Important Limitation +OneDrive storage **only supports work or school accounts** and requires admin permissions to authorize the API. Personal accounts cannot use this feature. +::: + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | - | Set to `onedrive` | +| `onedrive_domain` | string | `""` | Azure AD domain | +| `onedrive_client_id` | string | `""` | Application (client) ID | +| `onedrive_username` | string | `""` | Account email | +| `onedrive_password` | string | `""` | Account password | +| `onedrive_root_path` | string | `filebox_storage` | Storage root directory in OneDrive | +| `onedrive_proxy` | int | `0` | Download through server proxy | + +### Configuration Example + +```bash +file_storage=onedrive +onedrive_domain=contoso.onmicrosoft.com +onedrive_client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +onedrive_username=user@contoso.onmicrosoft.com +onedrive_password=your_password +onedrive_root_path=filebox_storage +``` + +### Azure App Registration Steps + +To use OneDrive storage, you need to register an application in the Azure portal: + +#### 1. Get Domain + +Log in to [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade), hover over your account in the top right corner, and the **Domain** shown in the popup is the `onedrive_domain` value. + +#### 2. Register Application + +1. Click **+ New registration** in the top left +2. Enter application name (e.g., FileCodeBox) +3. **Supported account types**: Select "Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts" +4. **Redirect URI**: Select `Web`, enter `http://localhost` +5. Click **Register** + +#### 3. Get Client ID + +After registration, find the **Application (client) ID** in the **Essentials** section on the app overview page. This is the `onedrive_client_id` value. + +#### 4. Configure Authentication + +1. Select **Authentication** in the left menu +2. Find **Allow public client flows**, select **Yes** +3. Click **Save** + +#### 5. Configure API Permissions + +1. Select **API permissions** in the left menu +2. Click **+ Add a permission** +3. Select **Microsoft Graph** → **Delegated permissions** +4. Check the following permissions: + - `openid` + - `Files.Read` + - `Files.Read.All` + - `Files.ReadWrite` + - `Files.ReadWrite.All` + - `User.Read` +5. Click **Add permissions** +6. Click **Grant admin consent for xxx** +7. After confirmation, permission status should show **Granted** + +### Install Dependencies + +Using OneDrive storage requires additional Python dependencies: + +```bash +pip install msal Office365-REST-Python-Client +``` + +### Verify Configuration + +You can use the following code to test if the configuration is correct: + +```python +import msal +from office365.graph_client import GraphClient + +domain = 'your_domain' +client_id = 'your_client_id' +username = 'your_username' +password = 'your_password' + +def acquire_token_pwd(): + authority_url = f'https://login.microsoftonline.com/{domain}' + app = msal.PublicClientApplication( + authority=authority_url, + client_id=client_id + ) + result = app.acquire_token_by_username_password( + username=username, + password=password, + scopes=['https://graph.microsoft.com/.default'] + ) + return result + +# Test connection +client = GraphClient(acquire_token_pwd) +me = client.me.get().execute_query() +print(f"Login successful: {me.user_principal_name}") +``` + + +## WebDAV Storage + +WebDAV storage supports saving files to any service that supports the WebDAV protocol, such as Nextcloud, ownCloud, Nutstore, etc. + +### Configuration Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `file_storage` | string | - | Set to `webdav` | +| `webdav_url` | string | `""` | WebDAV server URL | +| `webdav_username` | string | `""` | WebDAV username | +| `webdav_password` | string | `""` | WebDAV password | +| `webdav_root_path` | string | `filebox_storage` | Storage root directory in WebDAV | +| `webdav_proxy` | int | `0` | Download through server proxy | + +### Configuration Example + +```bash +file_storage=webdav +webdav_url=https://dav.example.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_password +webdav_root_path=filebox_storage +``` + +### Nextcloud Configuration Example + +```bash +file_storage=webdav +webdav_url=https://your-nextcloud.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip Nextcloud App Password +It's recommended to create an app password in Nextcloud instead of using your main password: +1. Log in to Nextcloud +2. Go to **Settings** → **Security** +3. Create a new app password in **Devices & sessions** +::: + +### Nutstore Configuration Example + +```bash +file_storage=webdav +webdav_url=https://dav.jianguoyun.com/dav/ +webdav_username=your_email@example.com +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip Nutstore App Password +Nutstore requires an app password: +1. Log in to Nutstore web version +2. Go to **Account Info** → **Security Options** +3. Add an app password +::: + +## OpenDAL Storage + +OpenDAL is a unified data access layer that supports multiple storage services. Through OpenDAL, you can use Google Cloud Storage, Azure Blob Storage, and more. + +### Configuration Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `file_storage` | string | Set to `opendal` | +| `opendal_scheme` | string | Storage service type (e.g., `gcs`, `azblob`) | +| `opendal__` | string | Service-specific configuration parameters | + +### Install Dependencies + +```bash +pip install opendal +``` + +### Google Cloud Storage Configuration Example + +```bash +file_storage=opendal +opendal_scheme=gcs +opendal_gcs_root=/filecodebox +opendal_gcs_bucket=your-bucket-name +opendal_gcs_credential=base64_encoded_credential +``` + +### Azure Blob Storage Configuration Example + +```bash +file_storage=opendal +opendal_scheme=azblob +opendal_azblob_root=/filecodebox +opendal_azblob_container=your-container +opendal_azblob_account_name=your_account +opendal_azblob_account_key=your_key +``` + +### Supported Services + +OpenDAL supports numerous storage services. For the complete list, see the [OpenDAL Official Documentation](https://opendal.apache.org/docs/rust/opendal/services/index.html). + +Common services include: +- `gcs` - Google Cloud Storage +- `azblob` - Azure Blob Storage +- `obs` - Huawei Cloud OBS +- `oss` - Aliyun OSS (via OpenDAL) +- `cos` - Tencent Cloud COS (via OpenDAL) +- `hdfs` - Hadoop HDFS +- `ftp` - FTP server +- `sftp` - SFTP server + +::: warning OpenDAL Notes +1. Services integrated via OpenDAL download through server proxy, consuming both storage service and server bandwidth +2. Compared to native S3/OneDrive support, OpenDAL may lack some debugging information +3. OpenDAL is written in Rust with good performance +::: + +## Storage Selection Recommendations + +| Scenario | Recommended Storage | Reason | +|----------|---------------------|--------| +| Personal/Small deployment | Local storage | Simple and easy, no extra configuration needed | +| Enterprise intranet | MinIO + S3 | Self-hosted object storage, data control | +| Public cloud deployment | Corresponding cloud provider S3 | Fast same-region access, low cost | +| Existing OneDrive | OneDrive | Utilize existing resources | +| Existing WebDAV | WebDAV | Good compatibility | +| Special storage needs | OpenDAL | Supports more storage services | + +## Common Issues + +### S3 Upload Failure + +1. Check if Access Key and Secret Key are correct +2. Confirm bucket name and region configuration are correct +3. Check bucket access permission settings +4. Confirm signature version (`s3v2` or `s3v4`) matches service provider requirements + +### OneDrive Authentication Failure + +1. Confirm using a work/school account, not a personal account +2. Check if Azure app has been granted admin consent +3. Confirm API permissions are fully configured +4. Verify username and password are correct + +### WebDAV Connection Failure + +1. Check if WebDAV URL format is correct +2. Confirm username and password (or app password) are correct +3. Check if server supports WebDAV protocol +4. Confirm network connection is normal diff --git a/docs/en/guide/upload copy.md b/docs/en/guide/upload copy.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/en/guide/upload.md b/docs/en/guide/upload.md index e69de29..25ce91e 100644 --- a/docs/en/guide/upload.md +++ b/docs/en/guide/upload.md @@ -0,0 +1,380 @@ +# File Upload + +FileCodeBox provides multiple flexible file upload methods, supporting both regular upload and chunked upload to meet different scenario requirements. + +## Upload Methods + +FileCodeBox supports the following upload methods: + +### Drag and Drop Upload + +Drag files directly to the upload area to start uploading. This is the most convenient upload method. + +1. Open the FileCodeBox homepage +2. Drag files from your file manager to the upload area +3. Release the mouse, file upload begins +4. Get the extraction code after upload completes + +::: tip Tip +Drag and drop upload supports dragging multiple files simultaneously (depending on theme support). +::: + +### Click Upload + +Click the upload area to select files through the system file picker. + +1. Click the "Select File" button in the upload area +2. Select the file to upload in the popup file picker +3. File upload begins after confirming selection +4. Get the extraction code after upload completes + +### Paste Upload + +Supports pasting images directly from clipboard for upload (supported by some themes). + +1. Copy an image to clipboard (screenshot or copy image) +2. Use `Ctrl+V` (Windows/Linux) or `Cmd+V` (macOS) to paste in the upload area +3. Image upload starts automatically +4. Get the extraction code after upload completes + +::: warning Note +Paste upload only supports image formats, not other file types. Specific support depends on the theme being used. +::: + +## File Size Limits + +### Default Limits + +| Setting | Default | Description | +|---------|---------|-------------| +| `uploadSize` | 10MB | Maximum single file upload size | + +### Modify Upload Limits + +Administrators can modify upload size limits through the admin panel or configuration file: + +```python +# Set maximum upload size to 100MB +uploadSize = 104857600 # 100 * 1024 * 1024 +``` + +::: info Note +`uploadSize` is in bytes. Common conversions: +- 10MB = 10485760 +- 50MB = 52428800 +- 100MB = 104857600 +- 500MB = 524288000 +- 1GB = 1073741824 +::: + +### Exceeding Limit Handling + +When an uploaded file exceeds the size limit, the system returns a 403 error: + +```json +{ + "detail": "Size exceeds limit, maximum is 10.00 MB" +} +``` + +## Regular Upload API + +### File Upload Endpoint + +**POST** `/share/file/` + +Content-Type: `multipart/form-data` + +**Request parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `file` | file | Yes | File to upload | +| `expire_value` | int | No | Expiration value, default 1 | +| `expire_style` | string | No | Expiration method, default `day` | + +**Expiration method options:** + +| Value | Description | +|-------|-------------| +| `day` | Expire by days | +| `hour` | Expire by hours | +| `minute` | Expire by minutes | +| `forever` | Never expire | +| `count` | Expire by download count | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + +**cURL example:** + +```bash +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" \ + -F "expire_value=7" \ + -F "expire_style=day" +``` + + +## Chunked Upload API + +For large files, FileCodeBox supports chunked upload functionality. Chunked upload splits large files into multiple small chunks for separate uploading, supporting resume capability. + +::: warning Prerequisite +Chunked upload functionality requires administrator enablement: `enableChunk=1` +::: + +### Chunked Upload Flow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ Initialize │ ──▶ │Upload Chunks│ ──▶ │ Complete │ +│ /init/ │ │ /chunk/ │ │ /complete/ │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌───────────┐ + │ Loop │ + │each chunk │ + └───────────┘ +``` + +### 1. Initialize Upload + +**POST** `/chunk/upload/init/` + +**Request parameters:** + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} +``` + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `file_name` | string | Yes | - | Filename | +| `file_size` | int | Yes | - | Total file size (bytes) | +| `chunk_size` | int | No | 5MB | Chunk size (bytes) | +| `file_hash` | string | Yes | - | SHA256 hash of the file | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456789", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +| Field | Description | +|-------|-------------| +| `existed` | Whether file already exists (instant upload) | +| `upload_id` | Upload session ID | +| `chunk_size` | Chunk size | +| `total_chunks` | Total number of chunks | +| `uploaded_chunks` | List of already uploaded chunk indices | + +### 2. Upload Chunk + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +**Path parameters:** + +| Parameter | Description | +|-----------|-------------| +| `upload_id` | Upload session ID returned during initialization | +| `chunk_index` | Chunk index, starting from 0 | + +**Request body:** + +Content-Type: `multipart/form-data` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `chunk` | file | Chunk data | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "a1b2c3d4e5f6..." + } +} +``` + +**cURL example:** + +```bash +# Upload first chunk (index 0) +curl -X POST "http://localhost:12345/chunk/upload/chunk/abc123def456789/0" \ + -F "chunk=@/path/to/chunk_0" +``` + +### 3. Complete Upload + +**POST** `/chunk/upload/complete/{upload_id}` + +**Path parameters:** + +| Parameter | Description | +|-----------|-------------| +| `upload_id` | Upload session ID | + +**Request parameters:** + +```json +{ + "expire_value": 7, + "expire_style": "day" +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `expire_value` | int | Yes | Expiration value | +| `expire_style` | string | Yes | Expiration method | + +**Response example:** + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### Resume Upload + +Chunked upload supports resume functionality. When upload is interrupted: + +1. Call the initialization endpoint again with the same `file_hash` +2. Server returns `uploaded_chunks` list containing already uploaded chunk indices +3. Client only needs to upload chunks not in the list +4. Call the complete endpoint after all chunks are uploaded + +**Example flow:** + +```javascript +// 1. Initialize upload +const initResponse = await fetch('/chunk/upload/init/', { + method: 'POST', + body: JSON.stringify({ + file_name: 'large_file.zip', + file_size: fileSize, + chunk_size: 5 * 1024 * 1024, + file_hash: fileHash + }) +}); +const { upload_id, uploaded_chunks, total_chunks } = await initResponse.json(); + +// 2. Upload incomplete chunks +for (let i = 0; i < total_chunks; i++) { + if (!uploaded_chunks.includes(i)) { + const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); + await fetch(`/chunk/upload/chunk/${upload_id}/${i}`, { + method: 'POST', + body: chunk + }); + } +} + +// 3. Complete upload +await fetch(`/chunk/upload/complete/${upload_id}`, { + method: 'POST', + body: JSON.stringify({ + expire_value: 7, + expire_style: 'day' + }) +}); +``` + +## Error Handling + +### Common Errors + +| HTTP Status | Error Message | Cause | Solution | +|-------------|---------------|-------|----------| +| 403 | Size exceeds limit | File exceeds `uploadSize` limit | Reduce file size or contact administrator to adjust limit | +| 403 | Upload rate limit | Exceeded IP upload rate limit | Wait for limit time window before retrying | +| 400 | Invalid expiration type | `expire_style` value not in allowed list | Use a valid expiration method | +| 404 | Upload session not found | `upload_id` invalid or expired | Re-initialize upload | +| 400 | Invalid chunk index | `chunk_index` out of range | Check if chunk index is correct | +| 400 | Incomplete chunks | Chunk count insufficient when completing upload | Ensure all chunks are uploaded | + +### Rate Limiting + +The system has rate limits on upload operations to prevent abuse: + +| Setting | Default | Description | +|---------|---------|-------------| +| `uploadMinute` | 1 | Limit time window (minutes) | +| `uploadCount` | 10 | Maximum uploads within time window | + +When rate limit is exceeded, you need to wait for the time window to pass before continuing uploads. + +### Error Response Format + +```json +{ + "detail": "Error message description" +} +``` + +## Upload Configuration + +### Related Settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `openUpload` | int | 1 | Enable upload (1=enabled, 0=disabled) | +| `uploadSize` | int | 10485760 | Maximum upload size (bytes) | +| `enableChunk` | int | 0 | Enable chunked upload (1=enabled, 0=disabled) | +| `uploadMinute` | int | 1 | Upload rate limit time window (minutes) | +| `uploadCount` | int | 10 | Maximum uploads within time window | +| `expireStyle` | list | ["day","hour","minute","forever","count"] | Allowed expiration methods | + +### Configuration Example + +```python +# Allow 100MB file uploads, enable chunked upload +uploadSize = 104857600 +enableChunk = 1 + +# Relax upload rate limit: max 50 uploads per 5 minutes +uploadMinute = 5 +uploadCount = 50 + +# Only allow expiration by days and count +expireStyle = ["day", "count"] +``` + +## Next Steps + +- [File Sharing](/en/guide/share) - Learn the complete sharing process +- [Configuration Guide](/en/guide/configuration) - Learn about all configuration options +- [Storage Configuration](/en/guide/storage) - Learn about file storage methods +- [Security Settings](/en/guide/security) - Learn about security-related configurations diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index e69de29..f32cd51 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -0,0 +1,206 @@ +# 配置说明 + +FileCodeBox 提供了丰富的配置选项,可以通过管理面板或直接修改配置来自定义系统行为。本文档详细介绍所有可用的配置项。 + +## 配置方式 + +FileCodeBox 支持两种配置方式: + +1. **管理面板配置**(推荐):访问 `/admin` 进入管理面板,在设置页面修改配置 +2. **数据库配置**:配置存储在 `data/filecodebox.db` 数据库中 + +::: tip 提示 +首次启动时,系统会使用 `core/settings.py` 中的默认配置。修改后的配置会保存到数据库中。 +::: + +## 基础设置 + +### 站点信息 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `name` | string | `文件快递柜 - FileCodeBox` | 站点名称,显示在页面标题和导航栏 | +| `description` | string | `开箱即用的文件快传系统` | 站点描述,用于 SEO | +| `keywords` | string | `FileCodeBox, 文件快递柜...` | 站点关键词,用于 SEO | +| `port` | int | `12345` | 服务监听端口 | + +### 通知设置 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `notify_title` | string | `系统通知` | 通知标题 | +| `notify_content` | string | 欢迎信息 | 通知内容,支持 HTML | +| `page_explain` | string | 法律声明 | 页面底部说明文字 | +| `robotsText` | string | `User-agent: *\nDisallow: /` | robots.txt 内容 | + +## 上传设置 + +### 文件上传限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `openUpload` | int | `1` | 是否开启上传功能(1=开启,0=关闭) | +| `uploadSize` | int | `10485760` | 单文件最大上传大小(字节),默认 10MB | +| `enableChunk` | int | `0` | 是否启用分片上传(1=启用,0=禁用) | + +::: warning 注意 +`uploadSize` 的单位是字节。10MB = 10 * 1024 * 1024 = 10485760 字节 +::: + +### 上传频率限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `uploadMinute` | int | `1` | 上传限制的时间窗口(分钟) | +| `uploadCount` | int | `10` | 在时间窗口内允许的最大上传次数 | + +例如:默认配置表示每 1 分钟内最多允许上传 10 次。 + +### 文件过期设置 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | 可选的过期方式 | +| `max_save_seconds` | int | `0` | 文件最大保存时间(秒),0 表示不限制 | + +过期方式说明: +- `day` - 按天过期 +- `hour` - 按小时过期 +- `minute` - 按分钟过期 +- `forever` - 永不过期 +- `count` - 按下载次数过期 + +## 主题设置 + +### 主题选择 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `themesSelect` | string | `themes/2024` | 当前使用的主题 | +| `themesChoices` | list | 见下方 | 可用主题列表 | + +默认可用主题: +```json +[ + { + "name": "2023", + "key": "themes/2023", + "author": "Lan", + "version": "1.0" + }, + { + "name": "2024", + "key": "themes/2024", + "author": "Lan", + "version": "1.0" + } +] +``` + +### 界面样式 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `opacity` | float | `0.9` | 界面透明度(0-1) | +| `background` | string | `""` | 自定义背景图片 URL,为空则使用默认背景 | + +## 管理员设置 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `admin_token` | string | `FileCodeBox2023` | 管理员登录密码 | +| `showAdminAddr` | int | `0` | 是否在首页显示管理入口(1=显示,0=隐藏) | + +::: danger 安全警告 +请务必在生产环境中修改默认的 `admin_token`!使用默认密码会导致严重的安全风险。 +::: + +## 安全设置 + +### 错误次数限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `errorMinute` | int | `1` | 错误限制的时间窗口(分钟) | +| `errorCount` | int | `1` | 在时间窗口内允许的最大错误次数 | + +此设置用于防止暴力破解提取码。 + +## 存储设置 + +### 存储类型 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `file_storage` | string | `local` | 存储后端类型 | +| `storage_path` | string | `""` | 自定义存储路径 | + +支持的存储类型: +- `local` - 本地存储 +- `s3` - S3 兼容存储(AWS S3、阿里云 OSS、MinIO 等) +- `onedrive` - OneDrive 存储 +- `webdav` - WebDAV 存储 +- `opendal` - OpenDAL 存储 + +详细的存储配置请参考 [存储配置](/guide/storage)。 + +## 配置示例 + +### 示例 1:小型个人使用 + +适合个人或小团队使用,限制较宽松: + +```python +{ + "name": "我的文件分享", + "uploadSize": 52428800, # 50MB + "uploadMinute": 5, # 5分钟 + "uploadCount": 20, # 最多20次 + "expireStyle": ["day", "hour", "forever"], + "admin_token": "your-secure-password", + "showAdminAddr": 1 +} +``` + +### 示例 2:公开服务 + +适合公开服务,需要更严格的限制: + +```python +{ + "name": "公共文件快递柜", + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, # 1分钟 + "uploadCount": 5, # 最多5次 + "errorMinute": 5, # 5分钟 + "errorCount": 3, # 最多3次错误 + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # 最长保存1天 + "admin_token": "very-secure-password-123", + "showAdminAddr": 0 +} +``` + +### 示例 3:企业内部使用 + +适合企业内部使用,支持大文件和分片上传: + +```python +{ + "name": "企业文件中转站", + "uploadSize": 1073741824, # 1GB + "enableChunk": 1, # 启用分片上传 + "uploadMinute": 10, # 10分钟 + "uploadCount": 100, # 最多100次 + "expireStyle": ["day", "forever"], + "file_storage": "s3", # 使用S3存储 + "admin_token": "enterprise-secure-token", + "showAdminAddr": 1 +} +``` + +## 下一步 + +- [存储配置](/guide/storage) - 了解如何配置不同的存储后端 +- [安全设置](/guide/security) - 了解如何增强系统安全性 +- [文件分享](/guide/share) - 了解文件分享功能 diff --git a/docs/guide/management.md b/docs/guide/management.md index e69de29..933fa4c 100644 --- a/docs/guide/management.md +++ b/docs/guide/management.md @@ -0,0 +1,412 @@ +# 管理面板 + +FileCodeBox 提供了功能完善的管理面板,让管理员可以方便地管理文件、查看系统状态和修改配置。本文档介绍管理面板的各项功能和使用方法。 + +## 访问管理面板 + +### 登录方式 + +管理面板位于 `/admin` 路径。访问方式: + +1. 在浏览器中访问 `http://your-domain.com/admin` +2. 输入管理员密码(`admin_token` 配置项的值) +3. 点击登录按钮 + +::: tip 提示 +默认管理员密码是 `FileCodeBox2023`。请务必在生产环境中修改此密码,详见 [安全设置](/guide/security)。 +::: + +### 显示管理入口 + +默认情况下,首页不显示管理面板入口。您可以通过配置控制是否显示: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `showAdminAddr` | int | `0` | 是否在首页显示管理入口(1=显示,0=隐藏) | + +::: warning 安全建议 +在公开服务中,建议保持 `showAdminAddr` 为 `0`,通过直接访问 `/admin` 路径进入管理面板,减少被恶意扫描的风险。 +::: + +### 认证机制 + +管理面板使用 JWT(JSON Web Token)进行身份认证: + +1. 登录成功后,服务器返回一个包含管理员身份的 Token +2. 后续请求通过 `Authorization: Bearer ` 头部携带 Token +3. Token 用于验证管理员身份,确保只有授权用户可以访问管理功能 + +## 仪表盘 + +登录后首先看到的是仪表盘页面,展示系统的整体运行状态。 + +### 统计指标 + +仪表盘显示以下关键指标: + +| 指标 | 说明 | +|------|------| +| **文件总数** (`totalFiles`) | 系统中存储的文件总数量 | +| **存储使用量** (`storageUsed`) | 所有文件占用的总存储空间(字节) | +| **系统运行时间** (`sysUptime`) | 系统首次启动的时间 | +| **昨日上传数** (`yesterdayCount`) | 昨天一整天上传的文件数量 | +| **昨日上传量** (`yesterdaySize`) | 昨天上传文件的总大小(字节) | +| **今日上传数** (`todayCount`) | 今天到目前为止上传的文件数量 | +| **今日上传量** (`todaySize`) | 今天上传文件的总大小(字节) | + +### 指标说明 + +- **文件总数**:包括所有未过期的文件和文本分享 +- **存储使用量**:显示实际文件占用的存储空间,不包括数据库等系统文件 +- **昨日/今日统计**:基于文件创建时间计算,用于了解系统使用趋势 + +::: tip 提示 +存储使用量显示的是字节数。例如 `10485760` 表示约 10MB。 +::: + +## 文件管理 + +### 文件列表 + +文件管理页面展示系统中所有已分享的文件,支持分页浏览和搜索。 + +**列表信息包括:** +- 文件 ID +- 提取码(code) +- 文件名前缀(prefix) +- 文件后缀(suffix) +- 文件大小 +- 创建时间 +- 过期时间 +- 剩余下载次数 + +### 搜索文件 + +使用搜索功能可以快速找到特定文件: + +1. 在搜索框中输入关键词 +2. 系统会根据文件名前缀(prefix)进行模糊匹配 +3. 搜索结果实时更新 + +**搜索示例:** +- 输入 `report` 可以找到所有文件名包含 "report" 的文件 +- 输入 `.pdf` 可以找到所有 PDF 文件(如果文件名包含此字符串) + +### 分页浏览 + +文件列表支持分页显示: + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `page` | `1` | 当前页码 | +| `size` | `10` | 每页显示数量 | + +### 删除文件 + +管理员可以删除任意文件: + +1. 在文件列表中找到要删除的文件 +2. 点击删除按钮 +3. 确认删除操作 + +::: danger 警告 +删除操作不可恢复!文件将从存储后端永久删除,同时删除数据库中的记录。 +::: + +**删除流程:** +1. 系统首先从存储后端(本地/S3/OneDrive 等)删除实际文件 +2. 然后从数据库中删除文件记录 +3. 删除后,对应的提取码将失效 + +### 下载文件 + +管理员可以直接下载任意文件: + +1. 在文件列表中找到目标文件 +2. 点击下载按钮 +3. 文件将通过浏览器下载 + +对于文本分享,系统会直接返回文本内容而不是下载文件。 + +### 修改文件信息 + +管理员可以修改已分享文件的部分信息: + +| 可修改字段 | 说明 | +|------------|------| +| `code` | 提取码(必须唯一,不能与其他文件重复) | +| `prefix` | 文件名前缀 | +| `suffix` | 文件后缀名 | +| `expired_at` | 过期时间 | +| `expired_count` | 剩余下载次数 | + +**修改提取码:** +``` +原提取码:abc123 +新提取码:myfile2024 +``` + +::: warning 注意 +修改提取码时,系统会检查新提取码是否已被使用。如果已存在相同的提取码,修改将失败。 +::: + +## 本地文件管理 + +除了管理已分享的文件,管理面板还提供了本地文件管理功能,用于管理 `data/local` 目录中的文件。 + +### 查看本地文件 + +本地文件列表显示 `data/local` 目录中的所有文件: + +| 信息 | 说明 | +|------|------| +| 文件名 | 文件的完整名称 | +| 创建时间 | 文件的创建时间 | +| 文件大小 | 文件大小(字节) | + +### 分享本地文件 + +可以将本地文件快速分享: + +1. 在本地文件列表中选择要分享的文件 +2. 设置过期方式和过期值 +3. 点击分享按钮 +4. 系统生成提取码 + +**分享参数:** + +| 参数 | 说明 | +|------|------| +| `filename` | 要分享的文件名 | +| `expire_style` | 过期方式(day/hour/minute/forever/count) | +| `expire_value` | 过期值(天数/小时数/分钟数/下载次数) | + +### 删除本地文件 + +可以删除 `data/local` 目录中的文件: + +1. 在本地文件列表中找到要删除的文件 +2. 点击删除按钮 +3. 确认删除 + +::: tip 使用场景 +本地文件管理功能适用于: +- 批量上传文件到服务器后进行分享 +- 管理通过其他方式上传到服务器的文件 +- 清理不需要的本地文件 +::: + +## 系统设置 + +### 查看配置 + +在系统设置页面可以查看当前所有配置项的值。配置项按类别分组显示: + +- 基础设置(站点名称、描述等) +- 上传设置(文件大小限制、频率限制等) +- 存储设置(存储类型、路径等) +- 主题设置(主题选择、透明度等) +- 安全设置(管理员密码、错误限制等) + +### 修改配置 + +管理员可以通过管理面板修改大部分配置: + +1. 进入系统设置页面 +2. 找到要修改的配置项 +3. 输入新的值 +4. 点击保存按钮 + +**可修改的配置项:** + +| 类别 | 配置项示例 | +|------|------------| +| 基础设置 | `name`, `description`, `keywords`, `notify_title`, `notify_content` | +| 上传设置 | `uploadSize`, `uploadMinute`, `uploadCount`, `openUpload`, `enableChunk` | +| 过期设置 | `expireStyle`, `max_save_seconds` | +| 主题设置 | `themesSelect`, `opacity`, `background` | +| 安全设置 | `admin_token`, `showAdminAddr`, `errorMinute`, `errorCount` | +| 存储设置 | `file_storage`, `storage_path` 及各存储后端的配置 | + +::: warning 注意 +- `admin_token`(管理员密码)不能设置为空 +- `themesChoices`(主题列表)不可通过管理面板修改 +- 修改存储设置后,已有文件不会自动迁移 +::: + +### 配置生效 + +配置修改后立即生效,无需重启服务。配置保存在数据库中,重启后仍然有效。 + +**配置存储位置:** +- 数据库:`data/filecodebox.db` +- 表名:`keyvalue` +- 键名:`settings` + +## API 接口 + +管理面板的所有功能都通过 REST API 实现,以下是主要接口: + +### 认证接口 + +**登录** +``` +POST /admin/login +Content-Type: application/json + +{ + "password": "your-admin-password" +} +``` + +响应: +```json +{ + "code": 200, + "detail": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "token_type": "Bearer" + } +} +``` + +### 仪表盘接口 + +**获取统计数据** +``` +GET /admin/dashboard +Authorization: Bearer +``` + +### 文件管理接口 + +**获取文件列表** +``` +GET /admin/file/list?page=1&size=10&keyword= +Authorization: Bearer +``` + +**删除文件** +``` +DELETE /admin/file/delete +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123 +} +``` + +**下载文件** +``` +GET /admin/file/download?id=123 +Authorization: Bearer +``` + +**修改文件信息** +``` +PATCH /admin/file/update +Authorization: Bearer +Content-Type: application/json + +{ + "id": 123, + "code": "newcode", + "expired_at": "2024-12-31T23:59:59" +} +``` + +### 本地文件接口 + +**获取本地文件列表** +``` +GET /admin/local/lists +Authorization: Bearer +``` + +**删除本地文件** +``` +DELETE /admin/local/delete +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt" +} +``` + +**分享本地文件** +``` +POST /admin/local/share +Authorization: Bearer +Content-Type: application/json + +{ + "filename": "example.txt", + "expire_style": "day", + "expire_value": 7 +} +``` + +### 配置接口 + +**获取配置** +``` +GET /admin/config/get +Authorization: Bearer +``` + +**更新配置** +``` +PATCH /admin/config/update +Authorization: Bearer +Content-Type: application/json + +{ + "admin_token": "new-password", + "uploadSize": 52428800 +} +``` + +## 常见问题 + +### 忘记管理员密码 + +如果忘记了管理员密码,可以通过以下方式重置: + +1. 停止 FileCodeBox 服务 +2. 使用 SQLite 工具打开 `data/filecodebox.db` +3. 查询 `keyvalue` 表中 `key='settings'` 的记录 +4. 修改 JSON 中的 `admin_token` 值 +5. 重启服务 + +```sql +-- 查看当前配置 +SELECT * FROM keyvalue WHERE key = 'settings'; + +-- 或者删除配置,恢复默认密码 +DELETE FROM keyvalue WHERE key = 'settings'; +``` + +### 文件删除失败 + +如果删除文件时出现错误,可能的原因: + +1. **存储后端连接失败**:检查存储配置是否正确 +2. **文件已不存在**:文件可能已被手动删除 +3. **权限不足**:检查存储目录的写入权限 + +### 配置修改不生效 + +如果修改配置后没有生效: + +1. 检查是否点击了保存按钮 +2. 刷新页面查看配置是否已保存 +3. 检查浏览器控制台是否有错误信息 +4. 确认配置值的格式是否正确(如数字类型不要输入字符串) + +## 下一步 + +- [配置说明](/guide/configuration) - 了解所有配置选项的详细说明 +- [安全设置](/guide/security) - 了解如何增强系统安全性 +- [存储配置](/guide/storage) - 配置不同的存储后端 diff --git a/docs/guide/security.md b/docs/guide/security.md index e69de29..d12c23f 100644 --- a/docs/guide/security.md +++ b/docs/guide/security.md @@ -0,0 +1,324 @@ +# 安全设置 + +FileCodeBox 提供了多层安全机制来保护您的文件分享服务。本文档介绍如何正确配置安全选项,确保系统安全运行。 + +## 管理员密码 + +### 修改默认密码 + +::: danger 重要安全警告 +FileCodeBox 的默认管理员密码是 `FileCodeBox2023`。**在生产环境中必须立即修改此密码!**使用默认密码会导致任何人都可以访问您的管理面板。 +::: + +修改管理员密码有两种方式: + +**方式一:通过管理面板修改(推荐)** + +1. 访问 `/admin` 进入管理面板 +2. 使用当前密码登录 +3. 进入「系统设置」页面 +4. 找到 `admin_token` 配置项 +5. 输入新的安全密码并保存 + +**方式二:通过数据库修改** + +配置存储在 `data/filecodebox.db` 数据库的 `keyvalue` 表中,可以直接修改 `admin_token` 的值。 + +### 密码安全建议 + +- 使用至少 16 个字符的强密码 +- 包含大小写字母、数字和特殊字符 +- 避免使用常见词汇或个人信息 +- 定期更换密码 + +```python +# 推荐的密码格式示例 +"admin_token": "Xk9#mP2$vL5@nQ8&wR3" +``` + +### 隐藏管理入口 + +默认情况下,管理面板入口是隐藏的。您可以通过 `showAdminAddr` 配置控制是否在首页显示管理入口: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `showAdminAddr` | int | `0` | 是否显示管理入口(1=显示,0=隐藏) | + +::: tip 建议 +在公开服务中,建议保持 `showAdminAddr` 为 `0`,通过直接访问 `/admin` 路径进入管理面板。 +::: + +## IP 速率限制 + +FileCodeBox 内置了基于 IP 的速率限制机制,可以有效防止滥用和攻击。 + +### 上传频率限制 + +限制单个 IP 在指定时间内的上传次数: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `uploadMinute` | int | `1` | 上传限制的时间窗口(分钟) | +| `uploadCount` | int | `10` | 在时间窗口内允许的最大上传次数 | + +**工作原理:** +- 系统记录每个 IP 的上传请求 +- 当某 IP 在 `uploadMinute` 分钟内的上传次数达到 `uploadCount` 时 +- 该 IP 的后续上传请求将被拒绝,返回 HTTP 423 错误 +- 等待时间窗口过期后,计数器重置 + +**配置示例:** + +```python +# 宽松配置:5分钟内最多上传20次 +{ + "uploadMinute": 5, + "uploadCount": 20 +} + +# 严格配置:1分钟内最多上传3次 +{ + "uploadMinute": 1, + "uploadCount": 3 +} +``` + +### 错误次数限制 + +限制单个 IP 的错误尝试次数,防止暴力破解提取码: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `errorMinute` | int | `1` | 错误限制的时间窗口(分钟) | +| `errorCount` | int | `1` | 在时间窗口内允许的最大错误次数 | + +**工作原理:** +- 当用户输入错误的提取码时,系统记录该 IP 的错误次数 +- 当错误次数达到 `errorCount` 时,该 IP 将被暂时锁定 +- 锁定时间为 `errorMinute` 分钟 +- 锁定期间,该 IP 的所有提取请求都将被拒绝 + +**配置示例:** + +```python +# 防暴力破解配置:5分钟内最多允许3次错误 +{ + "errorMinute": 5, + "errorCount": 3 +} +``` + +::: warning 注意 +默认配置 `errorMinute=1, errorCount=1` 非常严格,意味着输入一次错误的提取码后需要等待1分钟才能重试。根据实际需求调整此配置。 +::: + +## 上传限制 + +### 文件大小限制 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `uploadSize` | int | `10485760` | 单文件最大上传大小(字节),默认 10MB | +| `openUpload` | int | `1` | 是否开启上传功能(1=开启,0=关闭) | + +**常用大小换算:** +- 10MB = 10 * 1024 * 1024 = `10485760` +- 50MB = 50 * 1024 * 1024 = `52428800` +- 100MB = 100 * 1024 * 1024 = `104857600` +- 1GB = 1024 * 1024 * 1024 = `1073741824` + +### 文件过期设置 + +通过文件过期机制,可以自动清理过期文件,减少存储占用和安全风险: + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `expireStyle` | list | `["day","hour","minute","forever","count"]` | 可选的过期方式 | +| `max_save_seconds` | int | `0` | 文件最大保存时间(秒),0 表示不限制 | + +**过期方式说明:** +- `day` - 按天数过期 +- `hour` - 按小时过期 +- `minute` - 按分钟过期 +- `forever` - 永不过期(需要字符串提取码) +- `count` - 按下载次数过期 + +**安全建议:** + +对于公开服务,建议: +1. 移除 `forever` 选项,避免文件永久存储 +2. 设置 `max_save_seconds` 限制最长保存时间 +3. 优先使用 `count` 方式,下载后自动删除 + +```python +# 公开服务推荐配置 +{ + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400 # 最长保存1天 +} +``` + +### 关闭上传功能 + +在某些情况下,您可能需要临时关闭上传功能: + +```python +{ + "openUpload": 0 # 关闭上传功能 +} +``` + +## 反向代理安全配置 + +在生产环境中,通常会使用 Nginx 或其他反向代理服务器。以下是安全配置建议: + +### Nginx 配置示例 + +```nginx +server { + listen 80; + server_name your-domain.com; + + # 强制 HTTPS 重定向 + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name your-domain.com; + + # SSL 证书配置 + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers on; + + # 安全头部 + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # 限制请求体大小(与 uploadSize 配置一致) + client_max_body_size 100M; + + # 传递真实 IP + location / { + proxy_pass http://127.0.0.1:12345; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # 静态资源缓存 + location /assets { + proxy_pass http://127.0.0.1:12345; + proxy_cache_valid 200 7d; + add_header Cache-Control "public, max-age=604800"; + } +} +``` + +### 关键安全配置说明 + +**1. 传递真实 IP** + +FileCodeBox 的 IP 限制功能依赖于获取客户端真实 IP。系统会按以下顺序获取 IP: +1. `X-Real-IP` 请求头 +2. `X-Forwarded-For` 请求头 +3. 直接连接的客户端 IP + +确保反向代理正确设置这些头部: + +```nginx +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +``` + +**2. 请求体大小限制** + +Nginx 的 `client_max_body_size` 应该与 FileCodeBox 的 `uploadSize` 配置一致或略大: + +```nginx +client_max_body_size 100M; # 允许上传最大 100MB +``` + +**3. HTTPS 加密** + +强烈建议在生产环境中启用 HTTPS: +- 保护用户上传的文件内容 +- 保护管理员登录凭据 +- 防止中间人攻击 + +### Caddy 配置示例 + +```nginx +your-domain.com { + reverse_proxy localhost:12345 + + header { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + X-XSS-Protection "1; mode=block" + Strict-Transport-Security "max-age=31536000; includeSubDomains" + } +} +``` + +## 安全检查清单 + +部署 FileCodeBox 前,请确认以下安全配置: + +- [ ] 已修改默认管理员密码 `admin_token` +- [ ] 已隐藏管理入口 `showAdminAddr: 0` +- [ ] 已配置合适的上传频率限制 +- [ ] 已配置错误次数限制防止暴力破解 +- [ ] 已设置合理的文件大小限制 +- [ ] 已配置文件过期策略 +- [ ] 已启用 HTTPS 加密 +- [ ] 反向代理已正确传递真实 IP +- [ ] 已设置安全响应头部 + +## 推荐安全配置 + +### 公开服务配置 + +```python +{ + "admin_token": "your-very-secure-password", + "showAdminAddr": 0, + "uploadSize": 10485760, # 10MB + "uploadMinute": 1, + "uploadCount": 5, + "errorMinute": 5, + "errorCount": 3, + "expireStyle": ["hour", "minute", "count"], + "max_save_seconds": 86400, # 最长1天 + "openUpload": 1 +} +``` + +### 内部服务配置 + +```python +{ + "admin_token": "internal-secure-password", + "showAdminAddr": 1, + "uploadSize": 104857600, # 100MB + "uploadMinute": 5, + "uploadCount": 50, + "errorMinute": 1, + "errorCount": 5, + "expireStyle": ["day", "hour", "forever"], + "max_save_seconds": 0, # 不限制 + "openUpload": 1 +} +``` + +## 下一步 + +- [配置说明](/guide/configuration) - 了解所有配置选项 +- [存储配置](/guide/storage) - 配置安全的存储后端 +- [文件分享](/guide/share) - 了解文件分享功能 diff --git a/docs/guide/share.md b/docs/guide/share.md index e69de29..6ce8793 100644 --- a/docs/guide/share.md +++ b/docs/guide/share.md @@ -0,0 +1,341 @@ +# 文件分享 + +FileCodeBox 提供了简单易用的文件和文本分享功能。用户可以通过提取码安全地分享和获取文件。 + +## 分享方式 + +FileCodeBox 支持两种分享方式: + +1. **文本分享** - 直接分享文本内容,适合代码片段、配置文件等 +2. **文件分享** - 上传文件进行分享,支持各种文件格式 + +## 文本分享 + +### 使用方法 + +1. 在首页选择「文本分享」标签 +2. 在文本框中输入或粘贴要分享的内容 +3. 选择过期方式和时间 +4. 点击「分享」按钮 +5. 获取提取码 + +### 文本大小限制 + +::: warning 注意 +文本分享的最大内容大小为 **222KB**(227,328 字节)。如果内容超过此限制,建议使用文件分享方式。 +::: + +文本内容大小按 UTF-8 编码计算,中文字符通常占用 3 个字节。 + +### API 接口 + +**POST** `/share/text/` + +请求参数: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `text` | string | 是 | 要分享的文本内容 | +| `expire_value` | int | 否 | 过期数值,默认 1 | +| `expire_style` | string | 否 | 过期方式,默认 `day` | + +响应示例: + +```json +{ + "code": 200, + "detail": { + "code": "123456" + } +} +``` + +## 文件分享 + +### 使用方法 + +1. 在首页选择「文件分享」标签 +2. 点击上传区域或拖拽文件到上传区域 +3. 选择过期方式和时间 +4. 点击「上传」按钮 +5. 获取提取码 + +### 文件大小限制 + +默认单文件最大上传大小为 **10MB**。管理员可以通过 `uploadSize` 配置项修改此限制。 + +::: tip 提示 +如果需要上传大文件,请联系管理员启用分片上传功能,或调整 `uploadSize` 配置。 +::: + +### 支持的上传方式 + +- **点击上传** - 点击上传区域选择文件 +- **拖拽上传** - 将文件拖拽到上传区域 +- **粘贴上传** - 从剪贴板粘贴图片(部分主题支持) + +### API 接口 + +**POST** `/share/file/` + +请求参数(multipart/form-data): + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | file | 是 | 要上传的文件 | +| `expire_value` | int | 否 | 过期数值,默认 1 | +| `expire_style` | string | 否 | 过期方式,默认 `day` | + +响应示例: + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + +## 过期设置 + +FileCodeBox 支持多种灵活的过期方式: + +| 过期方式 | 参数值 | 说明 | +|----------|--------|------| +| 按天过期 | `day` | 文件在指定天数后过期 | +| 按小时过期 | `hour` | 文件在指定小时后过期 | +| 按分钟过期 | `minute` | 文件在指定分钟后过期 | +| 永不过期 | `forever` | 文件永久有效 | +| 按次数过期 | `count` | 文件在被下载指定次数后过期 | + +::: info 说明 +- 管理员可以通过 `expireStyle` 配置项控制用户可选的过期方式 +- 管理员可以通过 `max_save_seconds` 配置项限制文件的最长保存时间 +::: + +### 过期方式示例 + +```bash +# 文件 3 天后过期 +expire_value=3, expire_style=day + +# 文件 12 小时后过期 +expire_value=12, expire_style=hour + +# 文件 30 分钟后过期 +expire_value=30, expire_style=minute + +# 文件永不过期 +expire_value=1, expire_style=forever + +# 文件被下载 5 次后过期 +expire_value=5, expire_style=count +``` + +## 提取文件 + +### 使用方法 + +1. 在首页的「提取文件」区域输入提取码 +2. 点击「提取」按钮 +3. 系统会显示文件信息(文件名、大小等) +4. 点击「下载」按钮下载文件,或直接查看文本内容 + +### 提取码说明 + +- 提取码通常为 **6 位数字** +- 永不过期的文件使用 **字母数字混合** 的提取码 +- 提取码区分大小写(针对字母数字混合的情况) + +### API 接口 + +**查询文件信息** + +**POST** `/share/select/` + +请求参数: + +```json +{ + "code": "123456" +} +``` + +响应示例(文件): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "example.pdf", + "size": 1048576, + "text": "https://example.com/download/..." + } +} +``` + +响应示例(文本): + +```json +{ + "code": 200, + "detail": { + "code": "123456", + "name": "Text", + "size": 1024, + "text": "这是分享的文本内容..." + } +} +``` + +**直接下载文件** + +**GET** `/share/select/?code=123456` + +此接口会直接返回文件内容,适合在浏览器中直接访问。 + +## 分片上传(大文件) + +对于大文件上传,FileCodeBox 支持分片上传功能。此功能需要管理员启用(`enableChunk=1`)。 + +### 分片上传流程 + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务器 + + C->>S: 1. 初始化上传 (POST /chunk/upload/init/) + S-->>C: 返回 upload_id 和分片信息 + + loop 每个分片 + C->>S: 2. 上传分片 (POST /chunk/upload/chunk/{upload_id}/{chunk_index}) + S-->>C: 返回分片哈希 + end + + C->>S: 3. 完成上传 (POST /chunk/upload/complete/{upload_id}) + S-->>C: 返回提取码 +``` + +### 1. 初始化上传 + +**POST** `/chunk/upload/init/` + +请求参数: + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "sha256_hash_of_file" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file_name` | string | 是 | 文件名 | +| `file_size` | int | 是 | 文件总大小(字节) | +| `chunk_size` | int | 否 | 分片大小,默认 5MB | +| `file_hash` | string | 是 | 文件的 SHA256 哈希值 | + +响应示例: + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +### 2. 上传分片 + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +- `upload_id` - 初始化时返回的上传会话 ID +- `chunk_index` - 分片索引,从 0 开始 + +请求体:分片文件数据(multipart/form-data) + +响应示例: + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "sha256_hash_of_chunk" + } +} +``` + +### 3. 完成上传 + +**POST** `/chunk/upload/complete/{upload_id}` + +请求参数: + +```json +{ + "expire_value": 1, + "expire_style": "day" +} +``` + +响应示例: + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### 断点续传 + +分片上传支持断点续传。如果上传中断,可以: + +1. 重新调用初始化接口,使用相同的 `file_hash` +2. 服务器会返回已上传的分片列表 `uploaded_chunks` +3. 客户端只需上传未完成的分片 + +## 错误处理 + +### 常见错误码 + +| 错误码 | 说明 | 解决方案 | +|--------|------|----------| +| 403 | 文件大小超过限制 | 减小文件大小或联系管理员调整限制 | +| 403 | 内容过多 | 文本超过 222KB,请使用文件分享 | +| 403 | 上传频率限制 | 等待一段时间后重试 | +| 404 | 文件不存在 | 检查提取码是否正确 | +| 404 | 文件已过期 | 文件已过期或下载次数已用完 | + +### 频率限制 + +为防止滥用,系统对上传和提取操作有频率限制: + +- **上传限制**:默认每分钟最多 10 次上传 +- **错误限制**:默认每分钟最多 1 次错误尝试 + +::: tip 提示 +如果遇到频率限制,请等待限制时间窗口过后再重试。 +::: + +## 下一步 + +- [配置说明](/guide/configuration) - 了解如何配置分享相关设置 +- [存储配置](/guide/storage) - 了解文件存储方式 +- [安全设置](/guide/security) - 了解安全相关配置 +- [管理面板](/guide/management) - 了解如何管理分享的文件 diff --git a/docs/guide/storage.md b/docs/guide/storage.md index ab44cd8..86686c6 100644 --- a/docs/guide/storage.md +++ b/docs/guide/storage.md @@ -1,26 +1,395 @@ -# 阿里云设置 -S3 AccessKeyId: `AccessKeyId` +# 存储配置 -S3 SecretAccessKey: `SecretAccessKey` +FileCodeBox 支持多种存储后端,您可以根据需求选择合适的存储方式。本文档将详细介绍各种存储后端的配置方法。 -S3 BucketName: `bucket-name` +## 存储类型概览 -S3 EndpointUrl: `https://..aliyuncs.com` +| 存储类型 | 配置值 | 说明 | +|---------|--------|------| +| 本地存储 | `local` | 默认存储方式,文件保存在服务器本地 | +| S3 兼容存储 | `s3` | 支持 AWS S3、阿里云 OSS、MinIO 等 | +| OneDrive | `onedrive` | 微软 OneDrive 云存储(仅支持工作/学校账户) | +| WebDAV | `webdav` | 支持 WebDAV 协议的存储服务 | +| OpenDAL | `opendal` | 通过 OpenDAL 集成更多存储服务 | -S3 Signature Version: `s3v4` +## 本地存储 -S3 Region Name:`region` +本地存储是默认的存储方式,文件将保存在服务器的 `data/` 目录下。 -# Minio设置 +### 配置参数 -S3 AccessKeyId: `AccessKeyId` +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | `local` | 存储类型 | +| `storage_path` | string | `""` | 自定义存储路径(可选) | -S3 SecretAccessKey: `SecretAccessKey` +### 配置示例 -S3 BucketName: `bucket-name` +```bash +file_storage=local +storage_path= +``` -S3 EndpointUrl: api接口地址 +### 说明 -S3 Signature Version: `s3v4` +- 文件默认存储在 `data/share/data/` 目录下 +- 按日期自动创建子目录:`年/月/日/文件ID/` +- 建议在生产环境中将 `data/` 目录挂载到持久化存储 -S3 Region Name:根据`configurations`里面设置的 `Server Location` \ No newline at end of file +## S3 兼容存储 + +支持所有 S3 兼容的对象存储服务,包括 AWS S3、阿里云 OSS、MinIO、腾讯云 COS 等。 + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | - | 设置为 `s3` | +| `s3_access_key_id` | string | `""` | Access Key ID | +| `s3_secret_access_key` | string | `""` | Secret Access Key | +| `s3_bucket_name` | string | `""` | 存储桶名称 | +| `s3_endpoint_url` | string | `""` | S3 端点 URL | +| `s3_region_name` | string | `auto` | 区域名称 | +| `s3_signature_version` | string | `s3v2` | 签名版本(`s3v2` 或 `s3v4`) | +| `s3_hostname` | string | `""` | S3 主机名(备用) | +| `s3_proxy` | int | `0` | 是否通过服务器代理下载(1=是,0=否) | +| `aws_session_token` | string | `""` | AWS 会话令牌(可选) | + +### AWS S3 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=AKIAIOSFODNN7EXAMPLE +s3_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +s3_bucket_name=my-filecodebox-bucket +s3_endpoint_url=https://s3.amazonaws.com +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +### 阿里云 OSS 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=您的AccessKeyId +s3_secret_access_key=您的SecretAccessKey +s3_bucket_name=bucket-name +s3_endpoint_url=https://bucket-name.oss-cn-hangzhou.aliyuncs.com +s3_region_name=oss-cn-hangzhou +s3_signature_version=s3v4 +``` + +::: tip 阿里云 OSS 端点格式 +端点 URL 格式为:`https://..aliyuncs.com` + +常用区域: +- 杭州:`oss-cn-hangzhou` +- 上海:`oss-cn-shanghai` +- 北京:`oss-cn-beijing` +- 深圳:`oss-cn-shenzhen` +::: + +### MinIO 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=minioadmin +s3_secret_access_key=minioadmin +s3_bucket_name=filecodebox +s3_endpoint_url=http://localhost:9000 +s3_region_name=us-east-1 +s3_signature_version=s3v4 +``` + +::: warning MinIO 注意事项 +- `s3_endpoint_url` 填写 MinIO 的 API 接口地址 +- `s3_region_name` 根据 MinIO 配置中的 `Server Location` 设置 +- 确保存储桶已创建且有正确的访问权限 +::: + +### 腾讯云 COS 配置示例 + +```bash +file_storage=s3 +s3_access_key_id=您的SecretId +s3_secret_access_key=您的SecretKey +s3_bucket_name=bucket-name-1250000000 +s3_endpoint_url=https://cos.ap-guangzhou.myqcloud.com +s3_region_name=ap-guangzhou +s3_signature_version=s3v4 +``` + +### 代理下载 + +当 `s3_proxy=1` 时,文件下载将通过服务器中转,而不是直接从 S3 下载。这在以下情况下有用: + +- S3 存储桶不允许公开访问 +- 需要隐藏实际的存储地址 +- 网络环境限制直接访问 S3 + + + +## OneDrive 存储 + +OneDrive 存储支持将文件保存到微软 OneDrive 云存储。 + +::: warning 重要限制 +OneDrive 存储**仅支持工作或学校账户**,并且需要有管理员权限以授权 API。个人账户无法使用此功能。 +::: + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | - | 设置为 `onedrive` | +| `onedrive_domain` | string | `""` | Azure AD 域名 | +| `onedrive_client_id` | string | `""` | 应用程序(客户端)ID | +| `onedrive_username` | string | `""` | 账户邮箱 | +| `onedrive_password` | string | `""` | 账户密码 | +| `onedrive_root_path` | string | `filebox_storage` | OneDrive 中的存储根目录 | +| `onedrive_proxy` | int | `0` | 是否通过服务器代理下载 | + +### 配置示例 + +```bash +file_storage=onedrive +onedrive_domain=contoso.onmicrosoft.com +onedrive_client_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +onedrive_username=user@contoso.onmicrosoft.com +onedrive_password=your_password +onedrive_root_path=filebox_storage +``` + +### Azure 应用注册步骤 + +要使用 OneDrive 存储,您需要在 Azure 门户中注册应用程序: + +#### 1. 获取域名 + +登录 [Azure 门户](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade),将鼠标置于右上角账号处,浮窗显示的**域**即为 `onedrive_domain` 的值。 + +#### 2. 注册应用 + +1. 点击左上角的 **+ 新注册** +2. 输入应用名称(如:FileCodeBox) +3. **受支持的帐户类型**:选择"任何组织目录(任何 Azure AD 目录 - 多租户)中的帐户和个人 Microsoft 帐户" +4. **重定向 URI**:选择 `Web`,输入 `http://localhost` +5. 点击**注册** + +#### 3. 获取客户端 ID + +注册完成后,在应用概述页面的**概要**中找到**应用程序(客户端)ID**,即为 `onedrive_client_id` 的值。 + +#### 4. 配置身份验证 + +1. 在左侧菜单选择**身份验证** +2. 找到**允许公共客户端流**,选择**是** +3. 点击**保存** + +#### 5. 配置 API 权限 + +1. 在左侧菜单选择 **API 权限** +2. 点击 **+ 添加权限** +3. 选择 **Microsoft Graph** → **委托的权限** +4. 勾选以下权限: + - `openid` + - `Files.Read` + - `Files.Read.All` + - `Files.ReadWrite` + - `Files.ReadWrite.All` + - `User.Read` +5. 点击**添加权限** +6. 点击**代表 xxx 授予管理员同意** +7. 确认后,权限状态应显示为**已授予** + +### 安装依赖 + +使用 OneDrive 存储需要安装额外的 Python 依赖: + +```bash +pip install msal Office365-REST-Python-Client +``` + +### 验证配置 + +您可以使用以下代码测试配置是否正确: + +```python +import msal +from office365.graph_client import GraphClient + +domain = 'your_domain' +client_id = 'your_client_id' +username = 'your_username' +password = 'your_password' + +def acquire_token_pwd(): + authority_url = f'https://login.microsoftonline.com/{domain}' + app = msal.PublicClientApplication( + authority=authority_url, + client_id=client_id + ) + result = app.acquire_token_by_username_password( + username=username, + password=password, + scopes=['https://graph.microsoft.com/.default'] + ) + return result + +# 测试连接 +client = GraphClient(acquire_token_pwd) +me = client.me.get().execute_query() +print(f"登录成功:{me.user_principal_name}") +``` + +## WebDAV 存储 + +WebDAV 存储支持将文件保存到任何支持 WebDAV 协议的服务,如 Nextcloud、ownCloud、坚果云等。 + +### 配置参数 + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `file_storage` | string | - | 设置为 `webdav` | +| `webdav_url` | string | `""` | WebDAV 服务器 URL | +| `webdav_username` | string | `""` | WebDAV 用户名 | +| `webdav_password` | string | `""` | WebDAV 密码 | +| `webdav_root_path` | string | `filebox_storage` | WebDAV 中的存储根目录 | +| `webdav_proxy` | int | `0` | 是否通过服务器代理下载 | + +### 配置示例 + +```bash +file_storage=webdav +webdav_url=https://dav.example.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_password +webdav_root_path=filebox_storage +``` + +### Nextcloud 配置示例 + +```bash +file_storage=webdav +webdav_url=https://your-nextcloud.com/remote.php/dav/files/username/ +webdav_username=your_username +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip Nextcloud 应用密码 +建议在 Nextcloud 中创建应用密码,而不是使用主密码: +1. 登录 Nextcloud +2. 进入**设置** → **安全** +3. 在**设备与会话**中创建新的应用密码 +::: + +### 坚果云配置示例 + +```bash +file_storage=webdav +webdav_url=https://dav.jianguoyun.com/dav/ +webdav_username=your_email@example.com +webdav_password=your_app_password +webdav_root_path=FileCodeBox +``` + +::: tip 坚果云应用密码 +坚果云需要使用应用密码: +1. 登录坚果云网页版 +2. 进入**账户信息** → **安全选项** +3. 添加应用密码 +::: + +## OpenDAL 存储 + +OpenDAL 是一个统一的数据访问层,支持多种存储服务。通过 OpenDAL,您可以使用 Google Cloud Storage、Azure Blob Storage 等更多存储服务。 + +### 配置参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `file_storage` | string | 设置为 `opendal` | +| `opendal_scheme` | string | 存储服务类型(如 `gcs`、`azblob`) | +| `opendal__` | string | 服务特定的配置参数 | + +### 安装依赖 + +```bash +pip install opendal +``` + +### Google Cloud Storage 配置示例 + +```bash +file_storage=opendal +opendal_scheme=gcs +opendal_gcs_root=/filecodebox +opendal_gcs_bucket=your-bucket-name +opendal_gcs_credential=base64_encoded_credential +``` + +### Azure Blob Storage 配置示例 + +```bash +file_storage=opendal +opendal_scheme=azblob +opendal_azblob_root=/filecodebox +opendal_azblob_container=your-container +opendal_azblob_account_name=your_account +opendal_azblob_account_key=your_key +``` + +### 支持的服务 + +OpenDAL 支持众多存储服务,完整列表请参考 [OpenDAL 官方文档](https://opendal.apache.org/docs/rust/opendal/services/index.html)。 + +常用服务包括: +- `gcs` - Google Cloud Storage +- `azblob` - Azure Blob Storage +- `obs` - 华为云 OBS +- `oss` - 阿里云 OSS(通过 OpenDAL) +- `cos` - 腾讯云 COS(通过 OpenDAL) +- `hdfs` - Hadoop HDFS +- `ftp` - FTP 服务器 +- `sftp` - SFTP 服务器 + +::: warning OpenDAL 注意事项 +1. 通过 OpenDAL 集成的服务均通过服务器中转下载,会同时消耗存储服务和服务器的流量 +2. 相比原生 S3/OneDrive 支持,OpenDAL 方式可能缺少一些调试信息 +3. OpenDAL 采用 Rust 编写,性能较好 +::: + +## 存储选择建议 + +| 场景 | 推荐存储 | 原因 | +|------|----------|------| +| 个人/小型部署 | 本地存储 | 简单易用,无需额外配置 | +| 企业内网 | MinIO + S3 | 自建对象存储,数据可控 | +| 公有云部署 | 对应云厂商 S3 | 同区域访问快,成本低 | +| 已有 OneDrive | OneDrive | 利用现有资源 | +| 已有 WebDAV | WebDAV | 兼容性好 | +| 特殊存储需求 | OpenDAL | 支持更多存储服务 | + +## 常见问题 + +### S3 上传失败 + +1. 检查 Access Key 和 Secret Key 是否正确 +2. 确认存储桶名称和区域配置正确 +3. 检查存储桶的访问权限设置 +4. 确认签名版本(`s3v2` 或 `s3v4`)与服务商要求一致 + +### OneDrive 认证失败 + +1. 确认使用的是工作/学校账户,而非个人账户 +2. 检查 Azure 应用是否已授予管理员同意 +3. 确认 API 权限配置完整 +4. 验证用户名和密码是否正确 + +### WebDAV 连接失败 + +1. 检查 WebDAV URL 格式是否正确 +2. 确认用户名和密码(或应用密码)正确 +3. 检查服务器是否支持 WebDAV 协议 +4. 确认网络连接正常 diff --git a/docs/guide/upload.md b/docs/guide/upload.md index e69de29..81b1f26 100644 --- a/docs/guide/upload.md +++ b/docs/guide/upload.md @@ -0,0 +1,379 @@ +# 文件上传 + +FileCodeBox 提供了多种灵活的文件上传方式,支持普通上传和分片上传,满足不同场景的需求。 + +## 上传方式 + +FileCodeBox 支持以下几种上传方式: + +### 拖拽上传 + +将文件直接拖拽到上传区域即可开始上传。这是最便捷的上传方式。 + +1. 打开 FileCodeBox 首页 +2. 将文件从文件管理器拖拽到上传区域 +3. 松开鼠标,文件开始上传 +4. 上传完成后获取提取码 + +::: tip 提示 +拖拽上传支持同时拖拽多个文件(取决于主题支持)。 +::: + +### 点击上传 + +点击上传区域,通过系统文件选择器选择文件。 + +1. 点击上传区域的「选择文件」按钮 +2. 在弹出的文件选择器中选择要上传的文件 +3. 确认选择后文件开始上传 +4. 上传完成后获取提取码 + +### 粘贴上传 + +支持从剪贴板直接粘贴图片进行上传(部分主题支持)。 + +1. 复制图片到剪贴板(截图或复制图片) +2. 在上传区域使用 `Ctrl+V`(Windows/Linux)或 `Cmd+V`(macOS)粘贴 +3. 图片自动开始上传 +4. 上传完成后获取提取码 + +::: warning 注意 +粘贴上传仅支持图片格式,不支持其他文件类型。具体支持情况取决于所使用的主题。 +::: + +## 文件大小限制 + +### 默认限制 + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `uploadSize` | 10MB | 单文件最大上传大小 | + +### 修改上传限制 + +管理员可以通过管理面板或配置文件修改上传大小限制: + +```python +# 设置最大上传大小为 100MB +uploadSize = 104857600 # 100 * 1024 * 1024 +``` + +::: info 说明 +`uploadSize` 的单位是字节。常用换算: +- 10MB = 10485760 +- 50MB = 52428800 +- 100MB = 104857600 +- 500MB = 524288000 +- 1GB = 1073741824 +::: + +### 超出限制的处理 + +当上传文件超过大小限制时,系统会返回 403 错误: + +```json +{ + "detail": "大小超过限制,最大为10.00 MB" +} +``` + +## 普通上传 API + +### 文件上传接口 + +**POST** `/share/file/` + +Content-Type: `multipart/form-data` + +**请求参数:** + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | file | 是 | 要上传的文件 | +| `expire_value` | int | 否 | 过期数值,默认 1 | +| `expire_style` | string | 否 | 过期方式,默认 `day` | + +**过期方式选项:** + +| 值 | 说明 | +|----|------| +| `day` | 按天过期 | +| `hour` | 按小时过期 | +| `minute` | 按分钟过期 | +| `forever` | 永不过期 | +| `count` | 按下载次数过期 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "code": "654321", + "name": "example.pdf" + } +} +``` + +**cURL 示例:** + +```bash +curl -X POST "http://localhost:12345/share/file/" \ + -F "file=@/path/to/file.pdf" \ + -F "expire_value=7" \ + -F "expire_style=day" +``` + +## 分片上传 API + +对于大文件,FileCodeBox 支持分片上传功能。分片上传将大文件分割成多个小块分别上传,支持断点续传。 + +::: warning 前提条件 +分片上传功能需要管理员启用:`enableChunk=1` +::: + +### 分片上传流程 + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ 初始化上传 │ ──▶ │ 上传分片 │ ──▶ │ 完成上传 │ +│ /init/ │ │ /chunk/ │ │ /complete/ │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌───────────┐ + │ 循环上传 │ + │ 每个分片 │ + └───────────┘ +``` + +### 1. 初始化上传 + +**POST** `/chunk/upload/init/` + +**请求参数:** + +```json +{ + "file_name": "large_file.zip", + "file_size": 104857600, + "chunk_size": 5242880, + "file_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} +``` + +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| `file_name` | string | 是 | - | 文件名 | +| `file_size` | int | 是 | - | 文件总大小(字节) | +| `chunk_size` | int | 否 | 5MB | 分片大小(字节) | +| `file_hash` | string | 是 | - | 文件的 SHA256 哈希值 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "existed": false, + "upload_id": "abc123def456789", + "chunk_size": 5242880, + "total_chunks": 20, + "uploaded_chunks": [] + } +} +``` + +| 字段 | 说明 | +|------|------| +| `existed` | 文件是否已存在(秒传) | +| `upload_id` | 上传会话 ID | +| `chunk_size` | 分片大小 | +| `total_chunks` | 总分片数 | +| `uploaded_chunks` | 已上传的分片索引列表 | + +### 2. 上传分片 + +**POST** `/chunk/upload/chunk/{upload_id}/{chunk_index}` + +**路径参数:** + +| 参数 | 说明 | +|------|------| +| `upload_id` | 初始化时返回的上传会话 ID | +| `chunk_index` | 分片索引,从 0 开始 | + +**请求体:** + +Content-Type: `multipart/form-data` + +| 参数 | 类型 | 说明 | +|------|------|------| +| `chunk` | file | 分片数据 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "chunk_hash": "a1b2c3d4e5f6..." + } +} +``` + +**cURL 示例:** + +```bash +# 上传第一个分片(索引为 0) +curl -X POST "http://localhost:12345/chunk/upload/chunk/abc123def456789/0" \ + -F "chunk=@/path/to/chunk_0" +``` + +### 3. 完成上传 + +**POST** `/chunk/upload/complete/{upload_id}` + +**路径参数:** + +| 参数 | 说明 | +|------|------| +| `upload_id` | 上传会话 ID | + +**请求参数:** + +```json +{ + "expire_value": 7, + "expire_style": "day" +} +``` + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `expire_value` | int | 是 | 过期数值 | +| `expire_style` | string | 是 | 过期方式 | + +**响应示例:** + +```json +{ + "code": 200, + "detail": { + "code": "789012", + "name": "large_file.zip" + } +} +``` + +### 断点续传 + +分片上传支持断点续传。当上传中断后: + +1. 使用相同的 `file_hash` 重新调用初始化接口 +2. 服务器返回 `uploaded_chunks` 列表,包含已上传的分片索引 +3. 客户端只需上传不在列表中的分片 +4. 所有分片上传完成后调用完成接口 + +**示例流程:** + +```javascript +// 1. 初始化上传 +const initResponse = await fetch('/chunk/upload/init/', { + method: 'POST', + body: JSON.stringify({ + file_name: 'large_file.zip', + file_size: fileSize, + chunk_size: 5 * 1024 * 1024, + file_hash: fileHash + }) +}); +const { upload_id, uploaded_chunks, total_chunks } = await initResponse.json(); + +// 2. 上传未完成的分片 +for (let i = 0; i < total_chunks; i++) { + if (!uploaded_chunks.includes(i)) { + const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); + await fetch(`/chunk/upload/chunk/${upload_id}/${i}`, { + method: 'POST', + body: chunk + }); + } +} + +// 3. 完成上传 +await fetch(`/chunk/upload/complete/${upload_id}`, { + method: 'POST', + body: JSON.stringify({ + expire_value: 7, + expire_style: 'day' + }) +}); +``` + +## 错误处理 + +### 常见错误 + +| HTTP 状态码 | 错误信息 | 原因 | 解决方案 | +|-------------|----------|------|----------| +| 403 | 大小超过限制 | 文件超过 `uploadSize` 限制 | 减小文件大小或联系管理员调整限制 | +| 403 | 上传频率限制 | 超过 IP 上传频率限制 | 等待限制时间窗口后重试 | +| 400 | 过期时间类型错误 | `expire_style` 值不在允许列表中 | 使用有效的过期方式 | +| 404 | 上传会话不存在 | `upload_id` 无效或已过期 | 重新初始化上传 | +| 400 | 无效的分片索引 | `chunk_index` 超出范围 | 检查分片索引是否正确 | +| 400 | 分片不完整 | 完成上传时分片数量不足 | 确保所有分片都已上传 | + +### 频率限制 + +系统对上传操作有频率限制,防止滥用: + +| 配置项 | 默认值 | 说明 | +|--------|--------|------| +| `uploadMinute` | 1 | 限制时间窗口(分钟) | +| `uploadCount` | 10 | 时间窗口内最大上传次数 | + +当超过频率限制时,需要等待时间窗口过后才能继续上传。 + +### 错误响应格式 + +```json +{ + "detail": "错误信息描述" +} +``` + +## 上传配置 + +### 相关配置项 + +| 配置项 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `openUpload` | int | 1 | 是否开放上传(1=开放,0=关闭) | +| `uploadSize` | int | 10485760 | 最大上传大小(字节) | +| `enableChunk` | int | 0 | 是否启用分片上传(1=启用,0=禁用) | +| `uploadMinute` | int | 1 | 上传频率限制时间窗口(分钟) | +| `uploadCount` | int | 10 | 时间窗口内最大上传次数 | +| `expireStyle` | list | ["day","hour","minute","forever","count"] | 允许的过期方式 | + +### 配置示例 + +```python +# 允许上传 100MB 文件,启用分片上传 +uploadSize = 104857600 +enableChunk = 1 + +# 放宽上传频率限制:每 5 分钟最多 50 次 +uploadMinute = 5 +uploadCount = 50 + +# 只允许按天和按次数过期 +expireStyle = ["day", "count"] +``` + +## 下一步 + +- [文件分享](/guide/share) - 了解完整的分享流程 +- [配置说明](/guide/configuration) - 了解所有配置选项 +- [存储配置](/guide/storage) - 了解文件存储方式 +- [安全设置](/guide/security) - 了解安全相关配置