Full Code of vastsa/FileCodeBox for AI

master 1cc901d2b9ec cached
85 files
1.2 MB
347.3k tokens
1327 symbols
1 requests
Download .txt
Showing preview only (1,310K chars total). Download the full file or copy to clipboard to get everything.
Repository: vastsa/FileCodeBox
Branch: master
Commit: 1cc901d2b9ec
Files: 85
Total size: 1.2 MB

Directory structure:
gitextract_50fi8_rw/

├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── custom.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── docker-image.yml
│       └── docs.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── SECURITY.md
├── apps/
│   ├── __init__.py
│   ├── admin/
│   │   ├── __init__.py
│   │   ├── dependencies.py
│   │   ├── schemas.py
│   │   ├── services.py
│   │   └── views.py
│   └── base/
│       ├── __init__.py
│       ├── dependencies.py
│       ├── migrations/
│       │   ├── migrations_001.py
│       │   ├── migrations_002.py
│       │   ├── migrations_003.py
│       │   ├── migrations_004.py
│       │   └── migrations_005.py
│       ├── models.py
│       ├── schemas.py
│       ├── utils.py
│       └── views.py
├── core/
│   ├── __init__.py
│   ├── config.py
│   ├── database.py
│   ├── logger.py
│   ├── response.py
│   ├── settings.py
│   ├── storage.py
│   ├── tasks.py
│   └── utils.py
├── docker-compose.yml
├── docs/
│   ├── .vitepress/
│   │   ├── cache/
│   │   │   └── deps/
│   │   │       ├── _metadata.json
│   │   │       ├── chunk-CQOUZRMK.js
│   │   │       ├── chunk-KT7LHMJ2.js
│   │   │       ├── package.json
│   │   │       ├── vitepress___@vue_devtools-api.js
│   │   │       ├── vitepress___@vueuse_core.js
│   │   │       ├── vitepress___@vueuse_integrations_useFocusTrap.js
│   │   │       ├── vitepress___mark__js_src_vanilla__js.js
│   │   │       ├── vitepress___minisearch.js
│   │   │       └── vue.js
│   │   ├── config.mts
│   │   └── theme/
│   │       ├── custom.css
│   │       └── index.ts
│   ├── api/
│   │   ├── index.md
│   │   └── presign-upload.md
│   ├── changelog.md
│   ├── contributing.md
│   ├── en/
│   │   ├── api/
│   │   │   └── index.md
│   │   ├── changelog.md
│   │   ├── contributing.md
│   │   ├── guide/
│   │   │   ├── configuration.md
│   │   │   ├── getting-started.md
│   │   │   ├── introduction.md
│   │   │   ├── management.md
│   │   │   ├── security.md
│   │   │   ├── share.md
│   │   │   ├── storage.md
│   │   │   └── upload.md
│   │   ├── index.md
│   │   └── showcase.md
│   ├── guide/
│   │   ├── configuration.md
│   │   ├── getting-started.md
│   │   ├── introduction.md
│   │   ├── management.md
│   │   ├── security.md
│   │   ├── share.md
│   │   ├── storage-onedrive.md
│   │   ├── storage-opendal.md
│   │   ├── storage.md
│   │   └── upload.md
│   ├── index.md
│   ├── package.json
│   └── showcase.md
├── main.py
├── readme.md
├── readme_en.md
└── requirements.txt

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
# Git
.git
.gitignore
.gitattributes

# IDE
.idea
.vscode
*.swp
*.swo

# Documentation
*.md
docs/
LICENSE

# Development
.venv
__pycache__
*.pyc
*.pyo
.pytest_cache
.coverage
htmlcov/

# macOS
.DS_Store
._*

# Frontend source (will be built from GitHub)
fcb-fronted/
themes/

# Test files
test_*.py
tests/

# Data (should be mounted as volume)
data/

# GitHub
.github/

# Docker
Dockerfile
docker-compose.yml
.dockerignore

# Claude
.claude/


================================================
FILE: .gitattributes
================================================
*.js linguist-language=python
*.css linguist-language=python
*.html linguist-language=python


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/custom.md
================================================
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''

---




================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/docker-image.yml
================================================
name: Build and push Docker image

on:
  workflow_dispatch:
  push:
    branches:
      - master
      - dev
    tags:
      - 'v*'

env:
  REGISTRY_IMAGE: ${{ secrets.DOCKER_USERNAME }}/filecodebox

jobs:
  buildx:
    runs-on: ubuntu-latest
    timeout-minutes: 60
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY_IMAGE }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }}
            type=raw,value=beta,enable={{is_default_branch}}
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver-opts: |
            image=moby/buildkit:latest
            network=host

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          provenance: false


================================================
FILE: .github/workflows/docs.yml
================================================
# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程
#
name: Deploy VitePress site to Pages

on:
  # 在针对 `main` 分支的推送上运行。如果你
  # 使用 `master` 分支作为默认分支,请将其更改为 `master`
  push:
    branches: [ master ]

  # 允许你从 Actions 选项卡手动运行此工作流程
  workflow_dispatch:

# 设置 GITHUB_TOKEN 的权限,以允许部署到 GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
concurrency:
  group: pages
  cancel-in-progress: false

jobs:
  # 构建工作
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      # - uses: oven-sh/setup-bun@v1 # 如果使用 Bun,请取消注释
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
          cache-dependency-path: 'docs/pnpm-lock.yaml'
      - name: Setup Pages
        uses: actions/configure-pages@v4
      - name: Install dependencies
        working-directory: docs
        run: pnpm install
      - name: Build with VitePress
        working-directory: docs
        run: pnpm run docs:build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs/.vitepress/dist

  # 部署工作
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4


================================================
FILE: .gitignore
================================================
media/
logs/
.idea
/data
__pycache__/
*.py[cod]
*$py.class
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
database.db
# C extensions
*.so
*.env
# Distribution / packaging
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
.vite/
# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test.pdf / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/
*.db
./filecodebox.db-shm
./filecodebox.db-wal
# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
data/.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# Project
.vscode
.DS_Store
for_test.py
.html
/evaluate/temp.py
/evaluation/back.json
data/.env
.backup/
/cloc-1.64.exe

# Ignore node_modules
node_modules/

AGENTS.md

dist/
# Frontend themes (built from GitHub during Docker build)
themes/

================================================
FILE: Dockerfile
================================================
# 第一阶段:构建前端主题
FROM node:20-alpine AS frontend-builder

RUN apk add --no-cache git python3 make g++

WORKDIR /build

# 克隆并构建 2024 主题
RUN git clone --depth 1 https://github.com/vastsa/FileCodeBoxFronted.git /build/fronted-2024 && \
    cd /build/fronted-2024 && \
    npm install && \
    npm run build

# 克隆并构建 2023 主题
RUN git clone --depth 1 https://github.com/vastsa/FileCodeBoxFronted2023.git /build/fronted-2023 && \
    cd /build/fronted-2023 && \
    npm install --legacy-peer-deps && \
    npm run build

# 第二阶段:构建最终镜像
FROM python:3.12-slim-bookworm
LABEL author="Lan"
LABEL email="xzu@live.com"

WORKDIR /app

# 复制项目文件(通过 .dockerignore 排除不必要的文件)
COPY . .

# 设置时区
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo 'Asia/Shanghai' > /etc/timezone

# 从构建阶段复制编译好的前端主题
COPY --from=frontend-builder /build/fronted-2024/dist ./themes/2024
COPY --from=frontend-builder /build/fronted-2023/dist ./themes/2023

# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt

# 环境变量配置
ENV HOST="0.0.0.0" \
    PORT=12345 \
    WORKERS=1 \
    LOG_LEVEL="info"

EXPOSE 12345

# 生产环境启动命令
CMD uvicorn main:app \
    --host $HOST \
    --port $PORT \
    --workers $WORKERS \
    --log-level $LOG_LEVEL \
    --proxy-headers \
    --forwarded-allow-ips "*"

================================================
FILE: LICENSE
================================================
                   GNU LESSER GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.


  This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.

  0. Additional Definitions.

  As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.

  "The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.

  An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.

  A "Combined Work" is a work produced by combining or linking an
Application with the Library.  The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".

  The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.

  The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.

  1. Exception to Section 3 of the GNU GPL.

  You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.

  2. Conveying Modified Versions.

  If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:

   a) under this License, provided that you make a good faith effort to
   ensure that, in the event an Application does not supply the
   function or data, the facility still operates, and performs
   whatever part of its purpose remains meaningful, or

   b) under the GNU GPL, with none of the additional permissions of
   this License applicable to that copy.

  3. Object Code Incorporating Material from Library Header Files.

  The object code form of an Application may incorporate material from
a header file that is part of the Library.  You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:

   a) Give prominent notice with each copy of the object code that the
   Library is used in it and that the Library and its use are
   covered by this License.

   b) Accompany the object code with a copy of the GNU GPL and this license
   document.

  4. Combined Works.

  You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:

   a) Give prominent notice with each copy of the Combined Work that
   the Library is used in it and that the Library and its use are
   covered by this License.

   b) Accompany the Combined Work with a copy of the GNU GPL and this license
   document.

   c) For a Combined Work that displays copyright notices during
   execution, include the copyright notice for the Library among
   these notices, as well as a reference directing the user to the
   copies of the GNU GPL and this license document.

   d) Do one of the following:

       0) Convey the Minimal Corresponding Source under the terms of this
       License, and the Corresponding Application Code in a form
       suitable for, and under terms that permit, the user to
       recombine or relink the Application with a modified version of
       the Linked Version to produce a modified Combined Work, in the
       manner specified by section 6 of the GNU GPL for conveying
       Corresponding Source.

       1) Use a suitable shared library mechanism for linking with the
       Library.  A suitable mechanism is one that (a) uses at run time
       a copy of the Library already present on the user's computer
       system, and (b) will operate properly with a modified version
       of the Library that is interface-compatible with the Linked
       Version.

   e) Provide Installation Information, but only if you would otherwise
   be required to provide such information under section 6 of the
   GNU GPL, and only to the extent that such information is
   necessary to install and execute a modified version of the
   Combined Work produced by recombining or relinking the
   Application with a modified version of the Linked Version. (If
   you use option 4d0, the Installation Information must accompany
   the Minimal Corresponding Source and Corresponding Application
   Code. If you use option 4d1, you must provide the Installation
   Information in the manner specified by section 6 of the GNU GPL
   for conveying Corresponding Source.)

  5. Combined Libraries.

  You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:

   a) Accompany the combined library with a copy of the same work based
   on the Library, uncombined with any other library facilities,
   conveyed under the terms of this License.

   b) Give prominent notice with the combined library that part of it
   is a work based on the Library, and explaining where to find the
   accompanying uncombined form of the same work.

  6. Revised Versions of the GNU Lesser General Public License.

  The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.

  Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.

  If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

Use this section to tell people about which versions of your project are
currently being supported with security updates.

| Version | Supported          |
| ------- | ------------------ |
| 5.1.x   | :white_check_mark: |
| 5.0.x   | :x:                |
| 4.0.x   | :white_check_mark: |
| < 4.0   | :x:                |

## Reporting a Vulnerability

Use this section to tell people how to report a vulnerability.

Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.


================================================
FILE: apps/__init__.py
================================================
# @Time    : 2023/8/13 20:43
# @Author  : Lan
# @File    : __init__.py.py
# @Software: PyCharm


================================================
FILE: apps/admin/__init__.py
================================================
# @Time    : 2023/8/14 14:38
# @Author  : Lan
# @File    : __init__.py.py
# @Software: PyCharm


================================================
FILE: apps/admin/dependencies.py
================================================
# @Time    : 2023/8/15 17:43
# @Author  : Lan
# @File    : depends.py
# @Software: PyCharm
from fastapi import Header, HTTPException, Depends
from fastapi.requests import Request
import base64
import hmac
import json
import time
from core.settings import settings
from apps.admin.services import FileService, ConfigService, LocalFileService


def create_token(data: dict, expires_in: int = 3600 * 24 * 30) -> str:
    """
    创建JWT token
    :param data: 数据负载
    :param expires_in: 过期时间(秒)
    """
    header = base64.b64encode(
        json.dumps({"alg": "HS256", "typ": "JWT"}).encode()
    ).decode()
    payload = base64.b64encode(
        json.dumps({**data, "exp": int(time.time()) + expires_in}).encode()
    ).decode()

    signature = hmac.new(
        settings.admin_token.encode(), f"{header}.{payload}".encode(), "sha256"
    ).digest()
    signature = base64.b64encode(signature).decode()

    return f"{header}.{payload}.{signature}"


def verify_token(token: str) -> dict:
    """
    验证JWT token
    :param token: JWT token
    :return: 解码后的数据
    """
    try:
        header_b64, payload_b64, signature_b64 = token.split(".")

        # 验证签名
        expected_signature = hmac.new(
            settings.admin_token.encode(),
            f"{header_b64}.{payload_b64}".encode(),
            "sha256",
        ).digest()
        expected_signature_b64 = base64.b64encode(expected_signature).decode()

        if not hmac.compare_digest(signature_b64, expected_signature_b64):
            raise ValueError("无效的签名")

        # 解码payload
        payload = json.loads(base64.b64decode(payload_b64))

        # 检查是否过期
        if payload.get("exp", 0) < time.time():
            raise ValueError("token已过期")

        return payload
    except Exception as e:
        raise ValueError(f"token验证失败: {str(e)}")


def _extract_bearer_token(authorization: str) -> str:
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="未授权或授权校验失败")
    token = authorization.split(" ", 1)[1].strip()
    if not token:
        raise HTTPException(status_code=401, detail="未授权或授权校验失败")
    return token


def _require_admin_payload(authorization: str) -> dict:
    token = _extract_bearer_token(authorization)
    try:
        payload = verify_token(token)
    except ValueError as e:
        raise HTTPException(status_code=401, detail=str(e))
    if not payload.get("is_admin", False):
        raise HTTPException(status_code=401, detail="未授权或授权校验失败")
    return payload


ADMIN_PUBLIC_ENDPOINTS = {("POST", "/admin/login")}


async def admin_required(
    authorization: str = Header(default=None), request: Request = None
):
    """
    验证管理员权限
    """
    if request and (request.method, request.url.path) in ADMIN_PUBLIC_ENDPOINTS:
        return None
    return _require_admin_payload(authorization)


async def share_required_login(authorization: str = Header(default=None)):
    """
    验证分享上传权限
    
    当settings.openUpload为False时,要求用户必须登录并具有管理员权限
    当settings.openUpload为True时,允许游客上传
    
    :param authorization: 认证头信息
    :param request: 请求对象
    :return: 验证结果
    """
    if not settings.openUpload:
        if not authorization or not authorization.startswith("Bearer "):
            raise HTTPException(
                status_code=403, detail="本站未开启游客上传,如需上传请先登录后台"
            )
        _require_admin_payload(authorization)

    return True


async def get_file_service():
    return FileService()


async def get_config_service():
    return ConfigService()


async def get_local_file_service():
    return LocalFileService()


================================================
FILE: apps/admin/schemas.py
================================================
import datetime
from typing import Optional, Union

from pydantic import BaseModel


class IDData(BaseModel):
    id: int


class ShareItem(BaseModel):
    expire_value: int
    expire_style: str = "day"
    filename: str


class DeleteItem(BaseModel):
    filename: str


class LoginData(BaseModel):
    password: str


class UpdateFileData(BaseModel):
    id: int
    code: Optional[str] = None
    prefix: Optional[str] = None
    suffix: Optional[str] = None
    expired_at: Optional[Union[datetime.datetime, str]] = None
    expired_count: Optional[int] = None


================================================
FILE: apps/admin/services.py
================================================
import os
import time

from core.response import APIResponse
from core.storage import FileStorageInterface, storages
from core.settings import settings
from apps.base.models import FileCodes, KeyValue, file_codes_pydantic
from apps.base.utils import get_expire_info, get_file_path_name
from fastapi import HTTPException
from core.settings import data_root
from core.utils import hash_password, is_password_hashed


class FileService:
    def __init__(self):
        self.file_storage: FileStorageInterface = storages[settings.file_storage]()

    async def delete_file(self, file_id: int):
        file_code = await FileCodes.get(id=file_id)
        await self.file_storage.delete_file(file_code)
        await file_code.delete()

    async def list_files(self, page: int, size: int, keyword: str = ""):
        offset = (page - 1) * size
        files = (
            await FileCodes.filter(prefix__icontains=keyword).limit(size).offset(offset)
        )
        total = await FileCodes.filter(prefix__icontains=keyword).count()
        files_pydantic = [
            await file_codes_pydantic.from_tortoise_orm(f) for f in files
        ]
        return files_pydantic, total

    async def download_file(self, file_id: int):
        file_code = await FileCodes.filter(id=file_id).first()
        if not file_code:
            raise HTTPException(status_code=404, detail="文件不存在")
        if file_code.text:
            return APIResponse(detail=file_code.text)
        else:
            return await self.file_storage.get_file_response(file_code)

    async def share_local_file(self, item):
        local_file = LocalFileClass(item.filename)
        if not await local_file.exists():
            raise HTTPException(status_code=404, detail="文件不存在")

        text = await local_file.read()
        expired_at, expired_count, used_count, code = await get_expire_info(
            item.expire_value, item.expire_style
        )
        path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(item)

        await self.file_storage.save_file(text, save_path)

        await FileCodes.create(
            code=code,
            prefix=prefix,
            suffix=suffix,
            uuid_file_name=uuid_file_name,
            file_path=path,
            size=local_file.size,
            expired_at=expired_at,
            expired_count=expired_count,
            used_count=used_count,
        )

        return {
            "code": code,
            "name": local_file.file,
        }


class ConfigService:
    def get_config(self):
        return dict(settings.items())

    async def update_config(self, data: dict):
        admin_token = data.get("admin_token")
        if admin_token is None or admin_token == "":
            raise HTTPException(status_code=400, detail="管理员密码不能为空")

        if not is_password_hashed(admin_token):
            data["admin_token"] = hash_password(admin_token)

        for key, value in data.items():
            if key not in settings.default_config:
                continue
            if key in [
                "errorCount",
                "errorMinute",
                "max_save_seconds",
                "onedrive_proxy",
                "openUpload",
                "port",
                "s3_proxy",
                "uploadCount",
                "uploadMinute",
                "uploadSize",
            ]:
                data[key] = int(value)
            elif key in ["opacity"]:
                data[key] = float(value)
            else:
                data[key] = value

        await KeyValue.filter(key="settings").update(value=data)
        for k, v in data.items():
            settings.__setattr__(k, v)


class LocalFileService:
    async def list_files(self):
        files = []
        if not os.path.exists(data_root / "local"):
            os.makedirs(data_root / "local")
        for file in os.listdir(data_root / "local"):
            local_file = LocalFileClass(file)
            files.append({
                "file": local_file.file,
                "ctime": local_file.ctime,
                "size": local_file.size,
            })
        return files

    async def delete_file(self, filename: str):
        file = LocalFileClass(filename)
        if await file.exists():
            await file.delete()
            return "删除成功"
        raise HTTPException(status_code=404, detail="文件不存在")


class LocalFileClass:
    def __init__(self, file):
        self.file = file
        self.path = data_root / "local" / file
        if os.path.exists(self.path):
            self.ctime = time.strftime(
                "%Y-%m-%d %H:%M:%S", time.localtime(os.path.getctime(self.path))
            )
            self.size = os.path.getsize(self.path)
        else:
            self.ctime = None
            self.size = None

    async def read(self):
        return open(self.path, "rb")

    async def write(self, data):
        with open(self.path, "w") as f:
            f.write(data)

    async def delete(self):
        os.remove(self.path)

    async def exists(self):
        return os.path.exists(self.path)


================================================
FILE: apps/admin/views.py
================================================
# @Time    : 2023/8/14 14:38
# @Author  : Lan
# @File    : views.py
# @Software: PyCharm
import datetime

from fastapi import APIRouter, Depends, HTTPException
from apps.admin.services import FileService, ConfigService, LocalFileService
from apps.admin.dependencies import (
    admin_required,
    get_file_service,
    get_config_service,
    get_local_file_service,
)
from apps.admin.schemas import IDData, ShareItem, DeleteItem, LoginData, UpdateFileData
from core.response import APIResponse
from apps.base.models import FileCodes, KeyValue
from apps.admin.dependencies import create_token
from core.settings import settings
from core.utils import get_now, verify_password

admin_api = APIRouter(
    prefix="/admin", tags=["管理"], dependencies=[Depends(admin_required)]
)


@admin_api.post("/login")
async def login(data: LoginData):
    if not verify_password(data.password, settings.admin_token):
        raise HTTPException(status_code=401, detail="密码错误")

    token = create_token({"is_admin": True})
    return APIResponse(detail={"token": token, "token_type": "Bearer"})


@admin_api.get("/dashboard")
async def dashboard():
    all_codes = await FileCodes.all()
    all_size = str(sum([code.size for code in all_codes]))
    sys_start = await KeyValue.filter(key="sys_start").first()
    now = await get_now()
    today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
    yesterday_start = today_start - datetime.timedelta(days=1)
    yesterday_end = today_start - datetime.timedelta(microseconds=1)
    yesterday_codes = FileCodes.filter(
        created_at__gte=yesterday_start, created_at__lte=yesterday_end
    )
    today_codes = FileCodes.filter(created_at__gte=today_start)
    return APIResponse(
        detail={
            "totalFiles": len(all_codes),
            "storageUsed": all_size,
            "sysUptime": sys_start.value,
            "yesterdayCount": await yesterday_codes.count(),
            "yesterdaySize": str(sum([code.size for code in await yesterday_codes])),
            "todayCount": await today_codes.count(),
            "todaySize": str(sum([code.size for code in await today_codes])),
        }
    )


@admin_api.delete("/file/delete")
async def file_delete(
    data: IDData,
    file_service: FileService = Depends(get_file_service),
):
    await file_service.delete_file(data.id)
    return APIResponse()


@admin_api.get("/file/list")
async def file_list(
    page: int = 1,
    size: int = 10,
    keyword: str = "",
    file_service: FileService = Depends(get_file_service),
):
    files, total = await file_service.list_files(page, size, keyword)
    return APIResponse(
        detail={
            "page": page,
            "size": size,
            "data": files,
            "total": total,
        }
    )


@admin_api.get("/config/get")
async def get_config(
    config_service: ConfigService = Depends(get_config_service),
):
    return APIResponse(detail=config_service.get_config())


@admin_api.patch("/config/update")
async def update_config(
    data: dict,
    config_service: ConfigService = Depends(get_config_service),
):
    data.pop("themesChoices")
    await config_service.update_config(data)
    return APIResponse()


@admin_api.get("/file/download")
async def file_download(
    id: int,
    file_service: FileService = Depends(get_file_service),
):
    file_content = await file_service.download_file(id)
    return file_content


@admin_api.get("/local/lists")
async def get_local_lists(
    local_file_service: LocalFileService = Depends(get_local_file_service),
):
    files = await local_file_service.list_files()
    return APIResponse(detail=files)


@admin_api.delete("/local/delete")
async def delete_local_file(
    item: DeleteItem,
    local_file_service: LocalFileService = Depends(get_local_file_service),
):
    result = await local_file_service.delete_file(item.filename)
    return APIResponse(detail=result)


@admin_api.post("/local/share")
async def share_local_file(
    item: ShareItem,
    file_service: FileService = Depends(get_file_service),
):
    share_info = await file_service.share_local_file(item)
    return APIResponse(detail=share_info)


@admin_api.patch("/file/update")
async def update_file(
    data: UpdateFileData,
):
    file_code = await FileCodes.filter(id=data.id).first()
    if not file_code:
        raise HTTPException(status_code=404, detail="文件不存在")
    update_data = {}

    if data.code is not None and data.code != file_code.code:
        # 判断code是否存在
        if await FileCodes.filter(code=data.code).first():
            raise HTTPException(status_code=400, detail="code已存在")
        update_data["code"] = data.code
    if data.prefix is not None and data.prefix != file_code.prefix:
        update_data["prefix"] = data.prefix
    if data.suffix is not None and data.suffix != file_code.suffix:
        update_data["suffix"] = data.suffix
    if (
        data.expired_at is not None
        and data.expired_at != ""
        and data.expired_at != file_code.expired_at
    ):
        update_data["expired_at"] = data.expired_at
    if data.expired_count is not None and data.expired_count != file_code.expired_count:
        update_data["expired_count"] = data.expired_count

    await file_code.update_from_dict(update_data).save()
    return APIResponse(detail="更新成功")


================================================
FILE: apps/base/__init__.py
================================================
# @Time    : 2023/8/13 20:43
# @Author  : Lan
# @File    : __init__.py.py
# @Software: PyCharm


================================================
FILE: apps/base/dependencies.py
================================================
from typing import Dict, Union
from datetime import datetime, timedelta
from fastapi import HTTPException, Request


class IPRateLimit:
    def __init__(self, count: int, minutes: int):
        self.ips: Dict[str, Dict[str, Union[int, datetime]]] = {}
        self.count = count
        self.minutes = minutes

    def check_ip(self, ip: str) -> bool:
        if ip in self.ips:
            ip_info = self.ips[ip]
            if ip_info["count"] >= self.count:
                if ip_info["time"] + timedelta(minutes=self.minutes) > datetime.now():
                    return False
                self.ips.pop(ip)
        return True

    def add_ip(self, ip: str) -> int:
        ip_info = self.ips.get(ip, {"count": 0, "time": datetime.now()})
        ip_info["count"] += 1
        ip_info["time"] = datetime.now()
        self.ips[ip] = ip_info
        return ip_info["count"]

    async def remove_expired_ip(self) -> None:
        now = datetime.now()
        expiration = timedelta(minutes=self.minutes)
        self.ips = {
            ip: info
            for ip, info in self.ips.items()
            if info["time"] + expiration >= now
        }

    def __call__(self, request: Request) -> str:
        ip = (
            request.headers.get("X-Real-IP")
            or request.headers.get("X-Forwarded-For")
            or request.client.host
        )
        if not self.check_ip(ip):
            raise HTTPException(status_code=423, detail="请求次数过多,请稍后再试")
        return ip


================================================
FILE: apps/base/migrations/migrations_001.py
================================================
from tortoise import connections


async def create_file_codes_table():
    conn = connections.get("default")
    await conn.execute_script(
        """
        CREATE TABLE IF NOT EXISTS filecodes
        (
            id             INTEGER                                not null
                primary key autoincrement,
            code           VARCHAR(255)                           not null
                unique,
            prefix         VARCHAR(255) default ''                not null,
            suffix         VARCHAR(255) default ''                not null,
            uuid_file_name VARCHAR(255),
            file_path      VARCHAR(255),
            size           INT          default 0                 not null,
            text           TEXT,
            expired_at     TIMESTAMP,
            expired_count  INT          default 0                 not null,
            used_count     INT          default 0                 not null,
            created_at     TIMESTAMP    default CURRENT_TIMESTAMP not null
        );
        CREATE INDEX IF NOT EXISTS idx_filecodes_code_1c7ee7
            on filecodes (code);
    """
    )


async def create_key_value_table():
    conn = connections.get("default")
    await conn.execute_script(
        """
        CREATE TABLE IF NOT EXISTS keyvalue
        (
            id         INTEGER                             not null
                primary key autoincrement,
            key        VARCHAR(255)                        not null
                unique,
            value      JSON,
            created_at TIMESTAMP default CURRENT_TIMESTAMP not null
        );
        CREATE INDEX IF NOT EXISTS idx_keyvalue_key_eab890
            on keyvalue (key);
    """
    )


async def migrate():
    await create_file_codes_table()
    await create_key_value_table()


================================================
FILE: apps/base/migrations/migrations_002.py
================================================
from tortoise import connections


async def create_upload_chunk_and_update_file_codes_table():
    conn = connections.get("default")
    await conn.execute_script(
        """
        ALTER TABLE "filecodes" ADD "file_hash" VARCHAR(128);
        ALTER TABLE "filecodes" ADD "is_chunked" BOOL NOT NULL DEFAULT False;
        ALTER TABLE "filecodes" ADD "upload_id" VARCHAR(128);
        CREATE TABLE "uploadchunk" (
            id  INTEGER  not null   primary key autoincrement,
            "upload_id" VARCHAR(36) NOT NULL,
            "chunk_index" INT NOT NULL,
            "chunk_hash" VARCHAR(128) NOT NULL,
            "total_chunks" INT NOT NULL,
            "file_size" BIGINT NOT NULL,
            "chunk_size" INT NOT NULL,
            "created_at" TIMESTAMPTZ NOT NULL,
            "file_name" VARCHAR(255) NOT NULL,
            "completed" BOOL NOT NULL
        );
    """
    )


async def migrate():
    await create_upload_chunk_and_update_file_codes_table()


================================================
FILE: apps/base/migrations/migrations_003.py
================================================
from tortoise import connections


async def create_presign_upload_session_table():
    conn = connections.get("default")
    await conn.execute_script(
        """
        CREATE TABLE IF NOT EXISTS presignuploadsession (
            id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            upload_id VARCHAR(36) NOT NULL UNIQUE,
            file_name VARCHAR(255) NOT NULL,
            file_size BIGINT NOT NULL,
            save_path VARCHAR(512) NOT NULL,
            mode VARCHAR(10) NOT NULL,
            expire_value INT NOT NULL DEFAULT 1,
            expire_style VARCHAR(20) NOT NULL DEFAULT 'day',
            created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
            expires_at TIMESTAMP NOT NULL
        );
        CREATE INDEX IF NOT EXISTS idx_presignuploadsession_upload_id ON presignuploadsession (upload_id);
        """
    )


async def migrate():
    await create_presign_upload_session_table()


================================================
FILE: apps/base/migrations/migrations_004.py
================================================
from tortoise import connections


async def add_save_path_to_uploadchunk():
    conn = connections.get("default")
    await conn.execute_script(
        """
        ALTER TABLE uploadchunk ADD COLUMN save_path VARCHAR(512) NULL;
        """
    )


async def migrate():
    await add_save_path_to_uploadchunk()


================================================
FILE: apps/base/migrations/migrations_005.py
================================================
from tortoise import connections


def _need_upgrade(columns: list[tuple]) -> bool:
    for column in columns:
        # PRAGMA table_info 返回 (cid, name, type, notnull, dflt_value, pk)
        if column[1] == "size":
            column_type = (column[2] or "").upper()
            return "BIGINT" not in column_type
    return False


async def migrate():
    conn = connections.get("default")
    result = await conn.execute_query("PRAGMA table_info(filecodes)")
    columns = result[1] if result and len(result) > 1 else []

    if not columns or not _need_upgrade(columns):
        return

    await conn.execute_script(
        """
        BEGIN;
        CREATE TABLE IF NOT EXISTS filecodes_new
        (
            id             INTEGER                                not null
                primary key autoincrement,
            code           VARCHAR(255)                           not null
                unique,
            prefix         VARCHAR(255) default ''                not null,
            suffix         VARCHAR(255) default ''                not null,
            uuid_file_name VARCHAR(255),
            file_path      VARCHAR(255),
            size           BIGINT       default 0                 not null,
            text           TEXT,
            expired_at     TIMESTAMP,
            expired_count  INT          default 0                 not null,
            used_count     INT          default 0                 not null,
            created_at     TIMESTAMP    default CURRENT_TIMESTAMP not null,
            file_hash      VARCHAR(128),
            is_chunked     BOOL         default False             not null,
            upload_id      VARCHAR(128)
        );

        INSERT INTO filecodes_new (id, code, prefix, suffix, uuid_file_name, file_path, size, text,
                                   expired_at, expired_count, used_count, created_at, file_hash,
                                   is_chunked, upload_id)
        SELECT id,
               code,
               prefix,
               suffix,
               uuid_file_name,
               file_path,
               size,
               text,
               expired_at,
               expired_count,
               used_count,
               created_at,
               file_hash,
               is_chunked,
               upload_id
        FROM filecodes;

        DROP TABLE filecodes;
        ALTER TABLE filecodes_new
            RENAME TO filecodes;
        CREATE INDEX IF NOT EXISTS idx_filecodes_code_1c7ee7
            on filecodes (code);
        COMMIT;
        """
    )


================================================
FILE: apps/base/models.py
================================================
# @Time    : 2023/8/13 20:43
# @Author  : Lan
# @File    : models.py
# @Software: PyCharm
from typing import Optional

from tortoise.models import Model
from tortoise.contrib.pydantic import pydantic_model_creator

from tortoise import fields, models
from datetime import datetime
from core.utils import get_now


class FileCodes(models.Model):
    id = fields.IntField(pk=True)
    code = fields.CharField(max_length=255, unique=True, index=True)
    prefix = fields.CharField(max_length=255, default="")
    suffix = fields.CharField(max_length=255, default="")
    uuid_file_name = fields.CharField(max_length=255, null=True)
    file_path = fields.CharField(max_length=255, null=True)
    size = fields.BigIntField(default=0)
    text = fields.TextField(null=True)
    expired_at = fields.DatetimeField(null=True)
    expired_count = fields.IntField(default=0)
    used_count = fields.IntField(default=0)
    created_at = fields.DatetimeField(auto_now_add=True)
    file_hash = fields.CharField(max_length=64, null=True)
    is_chunked = fields.BooleanField(default=False)
    upload_id = fields.CharField(max_length=36, null=True)

    async def is_expired(self):
        if self.expired_at is None:
            return False
        if self.expired_at and self.expired_count < 0:
            return self.expired_at < await get_now()
        return self.expired_count <= 0

    async def get_file_path(self):
        return f"{self.file_path}/{self.uuid_file_name}"


class UploadChunk(models.Model):
    id = fields.IntField(pk=True)
    upload_id = fields.CharField(max_length=36, index=True)
    chunk_index = fields.IntField()
    chunk_hash = fields.CharField(max_length=64)
    total_chunks = fields.IntField()
    file_size = fields.BigIntField()
    chunk_size = fields.IntField()
    file_name = fields.CharField(max_length=255)
    save_path = fields.CharField(max_length=512, null=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    completed = fields.BooleanField(default=False)


class KeyValue(Model):
    id: Optional[int] = fields.IntField(pk=True)
    key: Optional[str] = fields.CharField(
        max_length=255, description="键", index=True, unique=True
    )
    value: Optional[str] = fields.JSONField(description="值", null=True)
    created_at: Optional[datetime] = fields.DatetimeField(
        auto_now_add=True, description="创建时间"
    )


class PresignUploadSession(models.Model):
    """预签名上传会话模型"""

    id = fields.IntField(pk=True)
    upload_id = fields.CharField(max_length=36, unique=True, index=True)
    file_name = fields.CharField(max_length=255)
    file_size = fields.BigIntField()
    save_path = fields.CharField(max_length=512)
    mode = fields.CharField(max_length=10)  # "direct" 或 "proxy"
    expire_value = fields.IntField(default=1)
    expire_style = fields.CharField(max_length=20, default="day")
    created_at = fields.DatetimeField(auto_now_add=True)
    expires_at = fields.DatetimeField()  # 会话过期时间

    async def is_expired(self):
        """检查会话是否已过期"""
        return self.expires_at < await get_now()


file_codes_pydantic = pydantic_model_creator(FileCodes, name="FileCodes")
upload_chunk_pydantic = pydantic_model_creator(UploadChunk, name="UploadChunk")
key_value_pydantic = pydantic_model_creator(KeyValue, name="KeyValue")
presign_upload_session_pydantic = pydantic_model_creator(
    PresignUploadSession, name="PresignUploadSession"
)


================================================
FILE: apps/base/schemas.py
================================================
from pydantic import BaseModel
from typing import Optional


class SelectFileModel(BaseModel):
    code: str


class InitChunkUploadModel(BaseModel):
    file_name: str
    chunk_size: int = 5 * 1024 * 1024
    file_size: int
    file_hash: str


class CompleteUploadModel(BaseModel):
    expire_value: int
    expire_style: str


# 预签名上传相关模型
class PresignUploadInitRequest(BaseModel):
    """预签名上传初始化请求"""
    file_name: str
    file_size: int
    expire_value: int = 1
    expire_style: str = "day"


class PresignUploadInitResponse(BaseModel):
    """预签名上传初始化响应"""
    upload_id: str
    upload_url: str
    mode: str  # "direct" 或 "proxy"
    save_path: str
    expires_in: int  # URL过期时间(秒)


================================================
FILE: apps/base/utils.py
================================================
import datetime
import hashlib
import os
import uuid
from urllib.parse import unquote

from fastapi import UploadFile, HTTPException
from typing import Optional, Tuple

from apps.base.dependencies import IPRateLimit
from apps.base.models import FileCodes
from core.settings import settings
from core.utils import (
    get_random_num,
    get_random_string,
    max_save_times_desc,
    sanitize_filename,
    get_now,
)


async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]:
    today = await get_now()
    storage_path = settings.storage_path.strip("/")
    file_uuid = uuid.uuid4().hex
    filename = await sanitize_filename(unquote(file.filename or ""))
    base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}"
    path = f"{storage_path}/{base_path}" if storage_path else base_path
    prefix, suffix = os.path.splitext(filename)
    save_path = f"{path}/{filename}"
    return path, suffix, prefix, filename, save_path


async def get_chunk_file_path_name(
    file_name: str, upload_id: str
) -> Tuple[str, str, str, str, str]:
    today = await get_now()
    storage_path = settings.storage_path.strip("/")
    base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{upload_id}"
    path = f"{storage_path}/{base_path}" if storage_path else base_path
    prefix, suffix = os.path.splitext(file_name)
    save_path = f"{path}/{prefix}{suffix}"
    return path, suffix, prefix, file_name, save_path


async def get_expire_info(
    expire_value: int, expire_style: str
) -> Tuple[Optional[datetime.datetime], int, int, str]:
    expired_count, used_count = -1, 0
    now = await get_now()
    code = None

    max_timedelta = (
        datetime.timedelta(seconds=settings.max_save_seconds)
        if settings.max_save_seconds > 0
        else datetime.timedelta(days=7)
    )
    detail = (
        await max_save_times_desc(settings.max_save_seconds)
        if settings.max_save_seconds > 0
        else "7天"
    )
    detail = f"限制最长时间为 {detail[0]},可换用其他方式"

    expire_styles = {
        "day": lambda: now + datetime.timedelta(days=expire_value),
        "hour": lambda: now + datetime.timedelta(hours=expire_value),
        "minute": lambda: now + datetime.timedelta(minutes=expire_value),
        "count": lambda: (now + datetime.timedelta(days=1), expire_value),
        "forever": lambda: (None, None),
    }

    if expire_style in expire_styles:
        result = expire_styles[expire_style]()
        if isinstance(result, tuple):
            expired_at, extra = result
            if expire_style == "count":
                expired_count = extra
            elif expire_style == "forever":
                code = await get_random_code(style="string")
        else:
            expired_at = result
        if expired_at and expired_at - now > max_timedelta:
            raise HTTPException(status_code=403, detail=detail)
    else:
        expired_at = now + datetime.timedelta(days=1)

    if not code:
        code = await get_random_code()

    return expired_at, expired_count, used_count, code


async def get_random_code(style: str = "num") -> str:
    while True:
        code = await get_random_num() if style == "num" else await get_random_string()
        if not await FileCodes.filter(code=code).exists():
            return str(code)


async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) -> str:
    sha = hashlib.sha256()
    await file.seek(0)
    while True:
        chunk = await file.read(chunk_size)
        if not chunk:
            break
        sha.update(chunk)
    await file.seek(0)
    return sha.hexdigest()


ip_limit = {
    "error": IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute),
    "upload": IPRateLimit(count=settings.uploadCount, minutes=settings.uploadMinute),
}


================================================
FILE: apps/base/views.py
================================================
import datetime
import hashlib
import os
import uuid
from datetime import timedelta
from urllib.parse import unquote

from typing import Optional, Tuple, Union

from fastapi import APIRouter, Form, UploadFile, File, Depends, HTTPException
from starlette import status

from apps.admin.dependencies import share_required_login
from apps.base.models import FileCodes, UploadChunk, PresignUploadSession
from apps.base.schemas import (
    SelectFileModel,
    InitChunkUploadModel,
    CompleteUploadModel,
    PresignUploadInitRequest,
)
from apps.base.utils import (
    get_expire_info,
    get_file_path_name,
    ip_limit,
    get_chunk_file_path_name,
)
from core.response import APIResponse
from core.settings import settings
from core.storage import storages, FileStorageInterface
from core.utils import get_select_token, get_now, sanitize_filename

share_api = APIRouter(prefix="/share", tags=["分享"])


# ============ 公共服务层 ============
class FileUploadService:
    """统一的文件上传服务"""

    @staticmethod
    async def generate_file_path(
        file_name: str, upload_id: Optional[str] = None
    ) -> tuple[str, str, str, str, str]:
        """统一的路径生成"""
        today = datetime.datetime.now()
        storage_path = settings.storage_path.strip("/")
        file_uuid = upload_id or uuid.uuid4().hex
        filename = await sanitize_filename(unquote(file_name))
        base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}"
        path = f"{storage_path}/{base_path}" if storage_path else base_path
        prefix, suffix = os.path.splitext(filename)
        save_path = f"{path}/{filename}"
        return path, suffix, prefix, filename, save_path

    @staticmethod
    async def create_file_record(
        file_name: str,
        file_size: int,
        file_path: str,
        expire_value: int,
        expire_style: str,
        **extra_fields,
    ) -> str:
        """统一创建FileCodes记录,返回code"""
        expired_at, expired_count, used_count, code = await get_expire_info(
            expire_value, expire_style
        )
        prefix, suffix = os.path.splitext(file_name)

        await FileCodes.create(
            code=code,
            prefix=prefix,
            suffix=suffix,
            uuid_file_name=file_name,
            file_path=file_path,
            size=file_size,
            expired_at=expired_at,
            expired_count=expired_count,
            used_count=used_count,
            **extra_fields,
        )
        return code


async def validate_file_size(file: UploadFile, max_size: int) -> int:
    size = file.size
    if size is None:
        await file.seek(0, 2)  # type: ignore[arg-type]
        size = file.file.tell()
        await file.seek(0)
    if size > max_size:
        max_size_mb = max_size / (1024 * 1024)
        raise HTTPException(
            status_code=403, detail=f"大小超过限制,最大为{max_size_mb:.2f} MB"
        )
    return size


async def create_file_code(code, **kwargs):
    return await FileCodes.create(code=code, **kwargs)


@share_api.post("/text/", dependencies=[Depends(share_required_login)])
async def share_text(
    text: str = Form(...),
    expire_value: int = Form(default=1, gt=0),
    expire_style: str = Form(default="day"),
    ip: str = Depends(ip_limit["upload"]),
):
    text_size = len(text.encode("utf-8"))
    max_txt_size = 222 * 1024
    if text_size > max_txt_size:
        raise HTTPException(status_code=403, detail="内容过多,建议采用文件形式")

    expired_at, expired_count, used_count, code = await get_expire_info(
        expire_value, expire_style
    )
    await create_file_code(
        code=code,
        text=text,
        expired_at=expired_at,
        expired_count=expired_count,
        used_count=used_count,
        size=len(text),
        prefix="Text",
    )
    ip_limit["upload"].add_ip(ip)
    return APIResponse(detail={"code": code})


@share_api.post("/file/", dependencies=[Depends(share_required_login)])
async def share_file(
    expire_value: int = Form(default=1, gt=0),
    expire_style: str = Form(default="day"),
    file: UploadFile = File(...),
    ip: str = Depends(ip_limit["upload"]),
):
    file_size = await validate_file_size(file, settings.uploadSize)
    if expire_style not in settings.expireStyle:
        raise HTTPException(status_code=400, detail="过期时间类型错误")
    expired_at, expired_count, used_count, code = await get_expire_info(
        expire_value, expire_style
    )
    path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(file)
    file_storage: FileStorageInterface = storages[settings.file_storage]()
    await file_storage.save_file(file, save_path)
    await create_file_code(
        code=code,
        prefix=prefix,
        suffix=suffix,
        uuid_file_name=uuid_file_name,
        file_path=path,
        size=file_size,
        expired_at=expired_at,
        expired_count=expired_count,
        used_count=used_count,
    )
    ip_limit["upload"].add_ip(ip)
    return APIResponse(detail={"code": code, "name": file.filename})


async def get_code_file_by_code(
    code: str, check: bool = True
) -> Tuple[bool, Union[FileCodes, str]]:
    file_code = await FileCodes.filter(code=code).first()
    if not file_code:
        return False, "文件不存在"
    if await file_code.is_expired() and check:
        return False, "文件已过期"
    return True, file_code


async def update_file_usage(file_code: FileCodes) -> None:
    file_code.used_count += 1
    if file_code.expired_count > 0:
        file_code.expired_count -= 1
    await file_code.save()


@share_api.get("/select/")
async def get_code_file(code: str, ip: str = Depends(ip_limit["error"])):
    file_storage: FileStorageInterface = storages[settings.file_storage]()
    has, file_code = await get_code_file_by_code(code)
    if not has:
        ip_limit["error"].add_ip(ip)
        return APIResponse(code=404, detail=file_code)

    assert isinstance(file_code, FileCodes)
    await update_file_usage(file_code)
    return await file_storage.get_file_response(file_code)


@share_api.post("/select/")
async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit["error"])):
    file_storage: FileStorageInterface = storages[settings.file_storage]()
    has, file_code = await get_code_file_by_code(data.code)
    if not has:
        ip_limit["error"].add_ip(ip)
        return APIResponse(code=404, detail=file_code)

    assert isinstance(file_code, FileCodes)
    await update_file_usage(file_code)
    return APIResponse(
        detail={
            "code": file_code.code,
            "name": file_code.prefix + file_code.suffix,
            "size": file_code.size,
            "text": (
                file_code.text
                if file_code.text is not None
                else await file_storage.get_file_url(file_code)
            ),
        }
    )


@share_api.get("/download")
async def download_file(key: str, code: str, ip: str = Depends(ip_limit["error"])):
    file_storage: FileStorageInterface = storages[settings.file_storage]()
    if await get_select_token(code) != key:
        ip_limit["error"].add_ip(ip)
        raise HTTPException(status_code=403, detail="下载鉴权失败")
    has, file_code = await get_code_file_by_code(code, False)
    if not has:
        return APIResponse(code=404, detail="文件不存在")
    assert isinstance(file_code, FileCodes)
    return (
        APIResponse(detail=file_code.text)
        if file_code.text
        else await file_storage.get_file_response(file_code)
    )


chunk_api = APIRouter(prefix="/chunk", tags=["切片"])


@chunk_api.post("/upload/init/", dependencies=[Depends(share_required_login)])
async def init_chunk_upload(data: InitChunkUploadModel):
    # 服务端校验:根据 total_chunks * chunk_size 计算理论最大上传量
    total_chunks = (data.file_size + data.chunk_size - 1) // data.chunk_size
    max_possible_size = total_chunks * data.chunk_size
    if max_possible_size > settings.uploadSize:
        max_size_mb = settings.uploadSize / (1024 * 1024)
        raise HTTPException(
            status_code=403, detail=f"文件大小超过限制,最大为 {max_size_mb:.2f} MB"
        )

    # # 秒传检查
    # existing = await FileCodes.filter(file_hash=data.file_hash).first()
    # if existing:
    #     if await existing.is_expired():
    #         file_storage: FileStorageInterface = storages[settings.file_storage](
    #         )
    #         await file_storage.delete_file(existing)
    #         await existing.delete()
    #     else:
    #         return APIResponse(detail={
    #             "code": existing.code,
    #             "existed": True,
    #             "name": f'{existing.prefix}{existing.suffix}'
    #         })

    # 断点续传:检查是否存在相同文件的未完成上传会话
    existing_session = await UploadChunk.filter(
        chunk_hash=data.file_hash,
        chunk_index=-1,
        file_size=data.file_size,
        file_name=data.file_name,
    ).first()

    if existing_session:
        if not existing_session.save_path:
            await UploadChunk.filter(upload_id=existing_session.upload_id).delete()
        else:
            uploaded_chunks = await UploadChunk.filter(
                upload_id=existing_session.upload_id, completed=True
            ).values_list("chunk_index", flat=True)
            return APIResponse(
                detail={
                    "existed": False,
                    "upload_id": existing_session.upload_id,
                    "chunk_size": existing_session.chunk_size,
                    "total_chunks": existing_session.total_chunks,
                    "uploaded_chunks": list(uploaded_chunks),
                }
            )

    # 创建新的上传会话
    upload_id = uuid.uuid4().hex
    _, _, _, _, save_path = await get_chunk_file_path_name(data.file_name, upload_id)
    await UploadChunk.create(
        upload_id=upload_id,
        chunk_index=-1,
        total_chunks=total_chunks,
        file_size=data.file_size,
        chunk_size=data.chunk_size,
        chunk_hash=data.file_hash,
        file_name=data.file_name,
        save_path=save_path,
    )
    return APIResponse(
        detail={
            "existed": False,
            "upload_id": upload_id,
            "chunk_size": data.chunk_size,
            "total_chunks": total_chunks,
            "uploaded_chunks": [],
        }
    )


@chunk_api.post(
    "/upload/chunk/{upload_id}/{chunk_index}",
    dependencies=[Depends(share_required_login)],
)
async def upload_chunk(
    upload_id: str,
    chunk_index: int,
    chunk: UploadFile = File(...),
):
    # 获取上传会话信息
    chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
    if not chunk_info:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在")

    # 检查分片索引有效性
    if chunk_index < 0 or chunk_index >= chunk_info.total_chunks:
        raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="无效的分片索引")

    # 检查是否已上传(支持断点续传)
    existing_chunk = await UploadChunk.filter(
        upload_id=upload_id, chunk_index=chunk_index, completed=True
    ).first()
    if existing_chunk:
        return APIResponse(
            detail={"chunk_hash": existing_chunk.chunk_hash, "skipped": True}
        )

    # 读取分片数据并计算哈希
    chunk_data = await chunk.read()
    chunk_size = len(chunk_data)

    # 校验分片大小不超过声明的 chunk_size
    if chunk_size > chunk_info.chunk_size:
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST,
            detail=f"分片大小超过声明值: 最大 {chunk_info.chunk_size}, 实际 {chunk_size}",
        )

    # 计算已上传分片数,校验累计大小不超限(用分片数 * chunk_size 估算)
    uploaded_count = await UploadChunk.filter(
        upload_id=upload_id, completed=True
    ).count()
    # 已上传分片的最大可能大小 + 当前分片
    max_uploaded_size = uploaded_count * chunk_info.chunk_size + chunk_size
    if max_uploaded_size > settings.uploadSize:
        max_size_mb = settings.uploadSize / (1024 * 1024)
        raise HTTPException(
            status_code=403, detail=f"累计上传大小超过限制,最大为 {max_size_mb:.2f} MB"
        )

    chunk_hash = hashlib.sha256(chunk_data).hexdigest()

    save_path = chunk_info.save_path

    # 保存分片到存储
    storage = storages[settings.file_storage]()
    try:
        await storage.save_chunk(
            upload_id, chunk_index, chunk_data, chunk_hash, save_path
        )
    except Exception as e:
        raise HTTPException(
            status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"分片保存失败: {str(e)}"
        )

    # 更新或创建分片记录(保存成功后再记录)
    await UploadChunk.update_or_create(
        upload_id=upload_id,
        chunk_index=chunk_index,
        defaults={
            "chunk_hash": chunk_hash,
            "completed": True,
            "file_size": chunk_info.file_size,
            "total_chunks": chunk_info.total_chunks,
            "chunk_size": chunk_info.chunk_size,
            "file_name": chunk_info.file_name,
            "save_path": chunk_info.save_path,
        },
    )
    return APIResponse(detail={"chunk_hash": chunk_hash})


@chunk_api.delete("/upload/{upload_id}", dependencies=[Depends(share_required_login)])
async def cancel_upload(upload_id: str):
    """取消上传并清理临时文件"""
    chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
    if not chunk_info:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在")

    save_path = chunk_info.save_path

    # 清理存储中的临时文件
    storage = storages[settings.file_storage]()
    if save_path:
        try:
            await storage.clean_chunks(upload_id, save_path)
        except Exception as e:
            pass

    # 清理数据库记录
    await UploadChunk.filter(upload_id=upload_id).delete()

    return APIResponse(detail={"message": "上传已取消"})


@chunk_api.get(
    "/upload/status/{upload_id}", dependencies=[Depends(share_required_login)]
)
async def get_upload_status(upload_id: str):
    """获取上传状态"""
    chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
    if not chunk_info:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在")

    # 获取已上传的分片列表
    uploaded_chunks = await UploadChunk.filter(
        upload_id=upload_id, completed=True
    ).values_list("chunk_index", flat=True)

    return APIResponse(
        detail={
            "upload_id": upload_id,
            "file_name": chunk_info.file_name,
            "file_size": chunk_info.file_size,
            "chunk_size": chunk_info.chunk_size,
            "total_chunks": chunk_info.total_chunks,
            "uploaded_chunks": list(uploaded_chunks),
            "progress": len(uploaded_chunks) / chunk_info.total_chunks * 100,
        }
    )


@chunk_api.post(
    "/upload/complete/{upload_id}", dependencies=[Depends(share_required_login)]
)
async def complete_upload(
    upload_id: str, data: CompleteUploadModel, ip: str = Depends(ip_limit["upload"])
):
    # 获取上传基本信息
    chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
    if not chunk_info:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在")

    storage = storages[settings.file_storage]()
    # 验证所有分片
    completed_chunks_list = await UploadChunk.filter(
        upload_id=upload_id, completed=True
    ).all()
    if len(completed_chunks_list) != chunk_info.total_chunks:
        raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="分片不完整")

    # 用分片数 * chunk_size 校验最大可能大小
    max_total_size = len(completed_chunks_list) * chunk_info.chunk_size
    if max_total_size > settings.uploadSize:
        save_path = chunk_info.save_path
        if save_path:
            try:
                await storage.clean_chunks(upload_id, save_path)
            except Exception:
                pass
        await UploadChunk.filter(upload_id=upload_id).delete()
        max_size_mb = settings.uploadSize / (1024 * 1024)
        raise HTTPException(
            status_code=403, detail=f"实际上传大小超过限制,最大为 {max_size_mb:.2f} MB"
        )

    save_path = chunk_info.save_path
    path = os.path.dirname(save_path) if save_path else ""
    prefix, suffix = os.path.splitext(chunk_info.file_name)

    try:
        # 合并文件并计算哈希
        _, file_hash = await storage.merge_chunks(upload_id, chunk_info, save_path)
        # 创建文件记录
        expired_at, expired_count, used_count, code = await get_expire_info(
            data.expire_value, data.expire_style
        )
        await FileCodes.create(
            code=code,
            file_hash=file_hash,  # 使用合并后计算的哈希
            is_chunked=True,
            upload_id=upload_id,
            size=chunk_info.file_size,
            expired_at=expired_at,
            expired_count=expired_count,
            used_count=used_count,
            file_path=path,
            uuid_file_name=f"{prefix}{suffix}",
            prefix=prefix,
            suffix=suffix,
        )
        # 清理临时文件
        await storage.clean_chunks(upload_id, save_path)
        # 清理数据库中的分片记录
        await UploadChunk.filter(upload_id=upload_id).delete()
        ip_limit["upload"].add_ip(ip)
        return APIResponse(detail={"code": code, "name": chunk_info.file_name})
    except ValueError as e:
        raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e))
    except Exception as e:
        # 合并失败时清理临时文件
        try:
            await storage.clean_chunks(upload_id, save_path)
        except Exception:
            pass
        raise HTTPException(
            status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件合并失败: {str(e)}"
        )


# ============ 预签名上传API ============
presign_api = APIRouter(prefix="/presign", tags=["预签名上传"])

PRESIGN_SESSION_EXPIRES = 900  # 15分钟


async def _get_valid_session(
    upload_id: str, expected_mode: Optional[str] = None
) -> PresignUploadSession:
    """获取并验证会话"""
    session = await PresignUploadSession.filter(upload_id=upload_id).first()
    if not session:
        raise HTTPException(404, "上传会话不存在")
    if await session.is_expired():
        await session.delete()
        raise HTTPException(404, "上传会话已过期")
    if expected_mode and session.mode != expected_mode:
        raise HTTPException(400, f"此会话不支持{expected_mode}模式")
    return session


@presign_api.post("/upload/init", dependencies=[Depends(share_required_login)])
async def presign_upload_init(
    data: PresignUploadInitRequest, ip: str = Depends(ip_limit["upload"])
):
    """初始化预签名上传,S3返回直传URL,其他存储返回代理URL"""
    if data.file_size > settings.uploadSize:
        raise HTTPException(
            403,
            f"文件大小超过限制,最大为 {settings.uploadSize / (1024 * 1024):.2f} MB",
        )
    if data.expire_style not in settings.expireStyle:
        raise HTTPException(400, "过期时间类型错误")

    upload_id = uuid.uuid4().hex
    path, _, _, filename, save_path = await FileUploadService.generate_file_path(
        data.file_name, upload_id
    )

    storage: FileStorageInterface = storages[settings.file_storage]()
    presigned_url = await storage.generate_presigned_upload_url(
        save_path, PRESIGN_SESSION_EXPIRES
    )

    mode = "direct" if presigned_url else "proxy"
    upload_url = presigned_url or f"/api/presign/upload/proxy/{upload_id}"

    await PresignUploadSession.create(
        upload_id=upload_id,
        file_name=filename,
        file_size=data.file_size,
        save_path=save_path,
        mode=mode,
        expire_value=data.expire_value,
        expire_style=data.expire_style,
        expires_at=await get_now() + timedelta(seconds=PRESIGN_SESSION_EXPIRES),
    )

    ip_limit["upload"].add_ip(ip)
    return APIResponse(
        detail={
            "upload_id": upload_id,
            "upload_url": upload_url,
            "mode": mode,
            "expires_in": PRESIGN_SESSION_EXPIRES,
        }
    )


@presign_api.put(
    "/upload/proxy/{upload_id}", dependencies=[Depends(share_required_login)]
)
async def presign_upload_proxy(
    upload_id: str, file: UploadFile = File(...), ip: str = Depends(ip_limit["upload"])
):
    """代理模式上传,服务器转存到存储后端"""
    session = await _get_valid_session(upload_id, expected_mode="proxy")

    file_size = await validate_file_size(file, settings.uploadSize)
    if abs(file_size - session.file_size) > 1024:
        raise HTTPException(400, "文件大小与声明不符")

    storage: FileStorageInterface = storages[settings.file_storage]()
    try:
        await storage.save_file(file, session.save_path)
    except Exception as e:
        raise HTTPException(500, f"文件保存失败: {str(e)}")

    code = await FileUploadService.create_file_record(
        session.file_name,
        file_size,
        os.path.dirname(session.save_path),
        session.expire_value,
        session.expire_style,
    )

    await session.delete()
    ip_limit["upload"].add_ip(ip)
    return APIResponse(detail={"code": code, "name": session.file_name})


@presign_api.post(
    "/upload/confirm/{upload_id}", dependencies=[Depends(share_required_login)]
)
async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upload"])):
    """直传确认,客户端完成S3直传后调用获取分享码"""
    session = await _get_valid_session(upload_id, expected_mode="direct")

    storage: FileStorageInterface = storages[settings.file_storage]()
    if not await storage.file_exists(session.save_path):
        raise HTTPException(404, "文件未上传或上传失败")

    code = await FileUploadService.create_file_record(
        session.file_name,
        session.file_size,
        os.path.dirname(session.save_path),
        session.expire_value,
        session.expire_style,
    )

    await session.delete()
    ip_limit["upload"].add_ip(ip)
    return APIResponse(detail={"code": code, "name": session.file_name})


@presign_api.get(
    "/upload/status/{upload_id}", dependencies=[Depends(share_required_login)]
)
async def presign_upload_status(upload_id: str):
    """查询上传会话状态"""
    session = await PresignUploadSession.filter(upload_id=upload_id).first()
    if not session:
        raise HTTPException(404, "上传会话不存在")

    return APIResponse(
        detail={
            "upload_id": session.upload_id,
            "file_name": session.file_name,
            "file_size": session.file_size,
            "mode": session.mode,
            "created_at": session.created_at.isoformat(),
            "expires_at": session.expires_at.isoformat(),
            "is_expired": await session.is_expired(),
        }
    )


@presign_api.delete("/upload/{upload_id}", dependencies=[Depends(share_required_login)])
async def presign_upload_cancel(upload_id: str):
    """取消上传会话"""
    session = await PresignUploadSession.filter(upload_id=upload_id).first()
    if not session:
        raise HTTPException(404, "上传会话不存在")

    if session.mode == "direct":
        storage: FileStorageInterface = storages[settings.file_storage]()
        try:
            if await storage.file_exists(session.save_path):
                temp_file_code = FileCodes(
                    file_path=os.path.dirname(session.save_path),
                    uuid_file_name=os.path.basename(session.save_path),
                )
                await storage.delete_file(temp_file_code)
        except Exception:
            pass

    await session.delete()
    return APIResponse(detail={"message": "上传会话已取消"})


================================================
FILE: core/__init__.py
================================================
# @Time    : 2023/8/11 20:06
# @Author  : Lan
# @File    : __init__.py.py
# @Software: PyCharm


================================================
FILE: core/config.py
================================================
from apps.base.models import KeyValue
from apps.base.utils import ip_limit
from core.settings import DEFAULT_CONFIG, settings


async def ensure_settings_row() -> None:
    await KeyValue.get_or_create(key="settings", defaults={"value": DEFAULT_CONFIG})


def _sync_ip_limits() -> None:
    ip_limit["error"].minutes = settings.errorMinute
    ip_limit["error"].count = settings.errorCount
    ip_limit["upload"].minutes = settings.uploadMinute
    ip_limit["upload"].count = settings.uploadCount


async def refresh_settings() -> None:
    """从数据库读取最新配置并应用到运行时。"""
    config_record = await KeyValue.filter(key="settings").first()
    settings.user_config = config_record.value if config_record and config_record.value else {}
    _sync_ip_limits()


================================================
FILE: core/database.py
================================================
import asyncio
import glob
import importlib
import os
from contextlib import asynccontextmanager
from typing import IO

from tortoise import Tortoise

from core.logger import logger
from core.settings import data_root


_DB_FILE = os.path.join(data_root, "filecodebox.db")
_STARTUP_LOCK_FILE = os.path.join(data_root, "filecodebox.startup.lock")


def get_db_config() -> dict:
    return {
        "connections": {
            "default": {
                "engine": "tortoise.backends.sqlite",
                "credentials": {
                    "file_path": _DB_FILE,
                    "journal_mode": "WAL",
                    "busy_timeout": 10000,
                    "foreign_keys": "ON",
                },
            }
        },
        "apps": {
            "models": {
                "models": ["apps.base.models"],
                "default_connection": "default",
            }
        },
        "use_tz": False,
        "timezone": "Asia/Shanghai",
    }


def _lock_file(file_obj: IO[str]) -> None:
    if os.name == "nt":
        import msvcrt

        # Windows 需要锁定至少 1 字节
        if os.fstat(file_obj.fileno()).st_size == 0:
            file_obj.write("0")
            file_obj.flush()
        msvcrt.locking(file_obj.fileno(), msvcrt.LK_LOCK, 1)
    else:
        import fcntl

        fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX)


def _unlock_file(file_obj: IO[str]) -> None:
    if os.name == "nt":
        import msvcrt

        msvcrt.locking(file_obj.fileno(), msvcrt.LK_UNLCK, 1)
    else:
        import fcntl

        fcntl.flock(file_obj.fileno(), fcntl.LOCK_UN)


@asynccontextmanager
async def db_startup_lock():
    os.makedirs(data_root, exist_ok=True)
    lock_file = open(_STARTUP_LOCK_FILE, "a+", encoding="utf-8")
    try:
        await asyncio.to_thread(_lock_file, lock_file)
        yield
    finally:
        await asyncio.to_thread(_unlock_file, lock_file)
        lock_file.close()


async def init_db():
    try:
        db_config = get_db_config()

        if not Tortoise._inited:
            await Tortoise.init(config=db_config)

        async with db_startup_lock():
            # 创建migrations表
            await Tortoise.get_connection("default").execute_script("""
                CREATE TABLE IF NOT EXISTS migrates (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    migration_file VARCHAR(255) NOT NULL UNIQUE,
                    executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)

            # 执行迁移
            await execute_migrations()

    except Exception as e:
        logger.error(f"数据库初始化失败: {str(e)}")
        raise


async def execute_migrations():
    """执行数据库迁移"""
    try:
        # 收集迁移文件
        migration_files = []
        for root, dirs, files in os.walk("apps"):
            if "migrations" in dirs:
                migration_path = os.path.join(root, "migrations")
                migration_files.extend(glob.glob(os.path.join(migration_path, "migrations_*.py")))

        # 按文件名排序
        migration_files.sort()

        for migration_file in migration_files:
            file_name = os.path.basename(migration_file)

            # 检查是否已执行
            executed = await Tortoise.get_connection("default").execute_query(
                "SELECT id FROM migrates WHERE migration_file = ?", [file_name]
            )

            if not executed[1]:
                logger.info(f"执行迁移: {file_name}")
                # 导入并执行migration
                module_path = migration_file.replace("/", ".").replace("\\", ".").replace(".py", "")
                try:
                    migration_module = importlib.import_module(module_path)
                    if hasattr(migration_module, "migrate"):
                        await migration_module.migrate()
                        # 记录执行
                        await Tortoise.get_connection("default").execute_query(
                            "INSERT INTO migrates (migration_file) VALUES (?)",
                            [file_name]
                        )
                        logger.info(f"迁移完成: {file_name}")
                except Exception as e:
                    logger.error(f"迁移 {file_name} 执行失败: {str(e)}")
                    raise

    except Exception as e:
        logger.error(f"迁移过程发生错误: {str(e)}")
        raise


================================================
FILE: core/logger.py
================================================
import logging
import sys


def setup_logger():
    # 创建logger对象
    _logger = logging.getLogger('FileCodeBox')
    _logger.setLevel(logging.INFO)

    # 创建控制台处理器
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)

    # 设置日志格式
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    console_handler.setFormatter(formatter)

    # 添加处理器到logger
    _logger.addHandler(console_handler)

    return _logger


# 创建全局logger实例
logger = setup_logger()


================================================
FILE: core/response.py
================================================
# @Time    : 2023/8/14 11:48
# @Author  : Lan
# @File    : response.py
# @Software: PyCharm
from typing import Generic, Optional, TypeVar

from pydantic import BaseModel

T = TypeVar("T")


class APIResponse(BaseModel, Generic[T]):
    code: int = 200
    message: str = "ok"
    detail: Optional[T] = None


================================================
FILE: core/settings.py
================================================
# @Time    : 2023/8/15 09:51
# @Author  : Lan
# @File    : settings.py
# @Software: PyCharm
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
data_root = BASE_DIR / "data"

if not data_root.exists():
    data_root.mkdir(parents=True, exist_ok=True)

DEFAULT_CONFIG = {
    "file_storage": "local",
    "storage_path": "",
    "name": "文件快递柜 - FileCodeBox",
    "description": "开箱即用的文件快传系统",
    "notify_title": "系统通知",
    "notify_content": '欢迎使用 FileCodeBox,本程序开源于 <a href="https://github.com/vastsa/FileCodeBox" target="_blank">Github</a> ,欢迎Star和Fork。',
    "page_explain": "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。",
    "keywords": "FileCodeBox, 文件快递柜, 口令传送箱, 匿名口令分享文本, 文件",
    "s3_access_key_id": "",
    "s3_secret_access_key": "",
    "s3_bucket_name": "",
    "s3_endpoint_url": "",
    "s3_region_name": "auto",
    "s3_signature_version": "s3v2",
    "s3_hostname": "",
    "s3_proxy": 0,
    "max_save_seconds": 0,
    "aws_session_token": "",
    "onedrive_domain": "",
    "onedrive_client_id": "",
    "onedrive_username": "",
    "onedrive_password": "",
    "onedrive_root_path": "filebox_storage",
    "onedrive_proxy": 0,
    "webdav_root_path": "filebox_storage",
    "webdav_proxy": 0,
    "admin_token": "FileCodeBox2023",
    "openUpload": 1,
    "uploadSize": 1024 * 1024 * 10,
    "expireStyle": ["day", "hour", "minute", "forever", "count"],
    "uploadMinute": 1,
    "enableChunk": 0,
    "webdav_url": "",
    "webdav_password": "",
    "webdav_username": "",
    "opacity": 0.9,
    "background": "",
    "uploadCount": 10,
    "themesChoices": [
        {
            "name": "2023",
            "key": "themes/2023",
            "author": "Lan",
            "version": "1.0",
        },
        {
            "name": "2024",
            "key": "themes/2024",
            "author": "Lan",
            "version": "1.0",
        },
    ],
    "themesSelect": "themes/2024",
    "errorMinute": 1,
    "errorCount": 10,
    "serverWorkers": 1,
    "serverHost": "0.0.0.0",
    "serverPort": 12345,
    "showAdminAddr": 0,
    "robotsText": "User-agent: *\nDisallow: /",
}


class Settings:
    def __init__(self, defaults=None):
        self.default_config = defaults or {}
        self.user_config = {}

    def __getattr__(self, attr):
        if attr in self.user_config:
            return self.user_config[attr]
        if attr in self.default_config:
            return self.default_config[attr]
        raise AttributeError(
            f"'{self.__class__.__name__}' object has no attribute '{attr}'"
        )

    def __setattr__(self, key, value):
        if key in ["default_config", "user_config"]:
            super().__setattr__(key, value)
        else:
            self.user_config[key] = value

    def items(self):
        return {**self.default_config, **self.user_config}.items()


settings = Settings(DEFAULT_CONFIG)


================================================
FILE: core/storage.py
================================================
# @Time    : 2023/8/11 20:06
# @Author  : Lan
# @File    : storage.py
# @Software: PyCharm
import base64
import hashlib
import os
import tempfile
from core.logger import logger
import shutil
from typing import Optional
from urllib.parse import quote, unquote

import aiofiles
import aiohttp
import asyncio
from pathlib import Path
import datetime
import re
import aioboto3
from botocore.config import Config
from fastapi import HTTPException, Response, UploadFile
from core.response import APIResponse
from core.settings import data_root, settings
from apps.base.models import FileCodes, UploadChunk
from core.utils import get_file_url, sanitize_filename
from fastapi.responses import FileResponse, StreamingResponse


class FileStorageInterface:

    async def save_file(self, file: UploadFile, save_path: str):
        """
        保存文件
        """
        raise NotImplementedError

    async def delete_file(self, file_code: FileCodes):
        """
        删除文件
        """
        raise NotImplementedError

    async def get_file_url(self, file_code: FileCodes):
        """
        获取文件分享的url

        如果服务不支持直接访问文件,可以通过服务器中转下载。
        此时,此方法可以调用 utils.py 中的 `get_file_url` 方法,获取服务器中转下载的url
        """
        raise NotImplementedError

    async def get_file_response(self, file_code: FileCodes):
        """
        获取文件响应

        如果服务不支持直接访问文件,则需要实现该方法,返回文件响应
        其余情况,可以不实现该方法
        """
        raise NotImplementedError

    async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str):
        """
        保存分片文件
        :param upload_id: 上传会话ID
        :param chunk_index: 分片索引
        :param chunk_data: 分片数据
        :param chunk_hash: 分片哈希值
        :param save_path: 文件保存路径
        """
        raise NotImplementedError

    async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]:
        """
        合并分片文件并返回文件路径和完整哈希值
        :param upload_id: 上传会话ID
        :param chunk_info: 分片信息
        :param save_path: 文件保存路径
        :return: (文件路径, 文件哈希值)
        """
        raise NotImplementedError

    async def generate_presigned_upload_url(self, save_path: str, expires_in: int = 900) -> Optional[str]:
        """
        生成预签名上传URL
        :param save_path: 文件保存路径
        :param expires_in: URL过期时间(秒),默认15分钟
        :return: 预签名URL,如果不支持直传则返回None
        """
        return None  # 默认不支持直传,使用代理模式

    async def file_exists(self, save_path: str) -> bool:
        """
        检查文件是否存在
        :param save_path: 文件路径
        :return: 文件是否存在
        """
        raise NotImplementedError

    async def clean_chunks(self, upload_id: str, save_path: str):
        """
        清理临时分片文件
        :param upload_id: 上传会话ID
        :param save_path: 文件保存路径
        """
        raise NotImplementedError


class SystemFileStorage(FileStorageInterface):
    def __init__(self):
        self.chunk_size = 256 * 1024
        self.root_path = data_root

    def _save(self, file, save_path):
        with open(save_path, "wb") as f:
            chunk = file.read(self.chunk_size)
            while chunk:
                f.write(chunk)
                chunk = file.read(self.chunk_size)

    async def save_file(self, file: UploadFile, save_path: str):
        path_obj = Path(save_path)
        directory = str(path_obj.parent)
        # 提取原始文件名并进行清理
        filename = await sanitize_filename(path_obj.name)
        # 构建安全的完整保存路径
        safe_save_path = self.root_path / directory / filename
        # 确保目录存在
        if not safe_save_path.parent.exists():
            safe_save_path.parent.mkdir(parents=True)
        await asyncio.to_thread(self._save, file.file, safe_save_path)

    async def delete_file(self, file_code: FileCodes):
        save_path = self.root_path / await file_code.get_file_path()
        if save_path.exists():
            save_path.unlink()

    async def get_file_url(self, file_code: FileCodes):
        return await get_file_url(file_code.code)

    async def get_file_response(self, file_code: FileCodes):
        file_path = self.root_path / await file_code.get_file_path()
        if not file_path.exists():
            return APIResponse(code=404, detail="文件已过期删除")
        filename = f"{file_code.prefix}{file_code.suffix}"
        encoded_filename = quote(filename, safe='')
        content_disposition = f"attachment; filename*=UTF-8''{encoded_filename}"
        
        # 尝试获取文件系统大小,如果成功则设置 Content-Length
        headers = {"Content-Disposition": content_disposition}
        try:
            content_length = file_path.stat().st_size
            headers["Content-Length"] = str(content_length)
        except Exception:
            # 如果获取文件大小失败,则不提供 Content-Length
            pass
        
        return FileResponse(
            file_path,
            media_type="application/octet-stream",
            headers=headers,
            filename=filename  # 保留原始文件名以备某些场景使用
        )

    async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str):
        """
        保存分片文件到本地文件系统
        :param upload_id: 上传会话ID
        :param chunk_index: 分片索引
        :param chunk_data: 分片数据
        :param chunk_hash: 分片哈希值
        :param save_path: 文件保存路径
        """
        chunk_dir = self.root_path / save_path
        chunk_path = chunk_dir.parent / 'chunks' / upload_id / f"{chunk_index}.part"
        if not chunk_path.parent.exists():
            chunk_path.parent.mkdir(parents=True, exist_ok=True)
        # 使用临时文件写入,确保原子性
        temp_path = chunk_path.with_suffix('.tmp')
        try:
            async with aiofiles.open(temp_path, "wb") as f:
                await f.write(chunk_data)
            # 原子重命名
            temp_path.rename(chunk_path)
        except Exception as e:
            if temp_path.exists():
                temp_path.unlink()
            raise e

    async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]:
        """
        合并本地文件系统的分片文件并返回文件路径和完整哈希值
        :param upload_id: 上传会话ID
        :param chunk_info: 分片信息
        :param save_path: 文件保存路径
        :return: (文件路径, 文件哈希值)
        """
        output_path = self.root_path / save_path
        output_path.parent.mkdir(parents=True, exist_ok=True)
        chunk_base_dir = output_path.parent / 'chunks' / upload_id
        file_sha256 = hashlib.sha256()
        
        # 使用临时文件写入,确保原子性
        temp_output = output_path.with_suffix('.merging')
        try:
            async with aiofiles.open(temp_output, "wb") as out_file:
                for i in range(chunk_info.total_chunks):
                    # 获取分片记录
                    chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first()
                    if not chunk_record:
                        raise ValueError(f"分片{i}记录不存在")
                    chunk_path = chunk_base_dir / f"{i}.part"
                    if not chunk_path.exists():
                        raise ValueError(f"分片{i}文件不存在")
                    async with aiofiles.open(chunk_path, "rb") as in_file:
                        chunk_data = await in_file.read()
                        current_hash = hashlib.sha256(chunk_data).hexdigest()
                        if current_hash != chunk_record.chunk_hash:
                            raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}")
                        file_sha256.update(chunk_data)
                        await out_file.write(chunk_data)
            # 原子重命名
            temp_output.rename(output_path)
        except Exception as e:
            if temp_output.exists():
                temp_output.unlink()
            raise e
        return str(output_path), file_sha256.hexdigest()

    async def clean_chunks(self, upload_id: str, save_path: str):
        """
        清理本地文件系统的临时分片文件
        :param upload_id: 上传会话ID
        :param save_path: 文件保存路径
        """
        chunk_dir = (self.root_path / save_path).parent / 'chunks' / upload_id
        if chunk_dir.exists():
            try:
                shutil.rmtree(chunk_dir)
            except Exception as e:
                logger.info(f"清理本地分片目录失败: {e}")
        # 清理父级 chunks 目录(如果为空)
        chunks_parent = chunk_dir.parent
        if chunks_parent.exists() and not any(chunks_parent.iterdir()):
            try:
                chunks_parent.rmdir()
            except Exception as e:
                logger.info(f"清理 chunks 父目录失败: {e}")

    async def file_exists(self, save_path: str) -> bool:
        """
        检查文件是否存在于本地文件系统
        :param save_path: 文件路径
        :return: 文件是否存在
        """
        file_path = self.root_path / save_path
        return file_path.exists()


class S3FileStorage(FileStorageInterface):
    def __init__(self):
        self.access_key_id = settings.s3_access_key_id
        self.secret_access_key = settings.s3_secret_access_key
        self.bucket_name = settings.s3_bucket_name
        self.s3_hostname = settings.s3_hostname
        self.region_name = settings.s3_region_name
        self.signature_version = settings.s3_signature_version
        self.endpoint_url = settings.s3_endpoint_url or f"https://{self.s3_hostname}"
        self.aws_session_token = settings.aws_session_token
        self.proxy = settings.s3_proxy
        self.session = aioboto3.Session(
            aws_access_key_id=self.access_key_id,
            aws_secret_access_key=self.secret_access_key,
        )
        if not settings.s3_endpoint_url:
            self.endpoint_url = f"https://{self.s3_hostname}"
        else:
            # 如果提供了 s3_endpoint_url,则优先使用它
            self.endpoint_url = settings.s3_endpoint_url

    async def save_file(self, file: UploadFile, save_path: str):
        async with self.session.client(
                "s3",
                endpoint_url=self.endpoint_url,
                aws_session_token=self.aws_session_token,
                region_name=self.region_name,
                config=Config(signature_version=self.signature_version),
        ) as s3:
            # 使用 upload_fileobj 流式上传,避免将整个文件加载到内存
            await s3.upload_fileobj(
                file.file,
                self.bucket_name,
                save_path,
                ExtraArgs={"ContentType": file.content_type or "application/octet-stream"},
            )

    async def delete_file(self, file_code: FileCodes):
        async with self.session.client(
                "s3",
                endpoint_url=self.endpoint_url,
                region_name=self.region_name,
                config=Config(signature_version=self.signature_version),
        ) as s3:
            await s3.delete_object(
                Bucket=self.bucket_name, Key=await file_code.get_file_path()
            )

    async def get_file_response(self, file_code: FileCodes):
        try:
            filename = file_code.prefix + file_code.suffix
            content_length = None  # 初始化为 None,表示未知大小
            
            async with self.session.client(
                    "s3",
                    endpoint_url=self.endpoint_url,
                    region_name=self.region_name,
                    config=Config(signature_version=self.signature_version),
            ) as s3:
                # 尝试获取文件大小(HEAD请求)
                try:
                    head_response = await s3.head_object(
                        Bucket=self.bucket_name,
                        Key=await file_code.get_file_path()
                    )
                    # 从HEAD响应中获取Content-Length
                    if 'ContentLength' in head_response:
                        content_length = head_response['ContentLength']
                    elif 'Content-Length' in head_response['ResponseMetadata']['HTTPHeaders']:
                        content_length = int(head_response['ResponseMetadata']['HTTPHeaders']['Content-Length'])
                except Exception:
                    # 如果HEAD请求失败,则不提供 Content-Length
                    pass
                
                link = await s3.generate_presigned_url(
                    "get_object",
                    Params={
                        "Bucket": self.bucket_name,
                        "Key": await file_code.get_file_path(),
                    },
                    ExpiresIn=3600,
                )
            
            # 创建ClientSession并传递给生成器复用
            session = aiohttp.ClientSession()
            
            async def stream_generator():
                try:
                    async with session.get(link) as resp:
                        if resp.status != 200:
                            raise HTTPException(
                                status_code=resp.status,
                                detail=f"从S3获取文件失败: {resp.status}"
                            )
                        # 设置块大小(例如64KB)
                        chunk_size = 65536
                        while True:
                            chunk = await resp.content.read(chunk_size)
                            if not chunk:
                                break
                            yield chunk
                finally:
                    await session.close()
            
            from fastapi.responses import StreamingResponse
            encoded_filename = quote(filename, safe='')
            headers = {
                "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
            }
            if content_length is not None:
                headers["Content-Length"] = str(content_length)
            return StreamingResponse(
                stream_generator(),
                media_type="application/octet-stream",
                headers=headers
            )
        except HTTPException:
            raise
        except Exception:
            raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试")

    async def get_file_url(self, file_code: FileCodes):
        if file_code.prefix == "文本分享":
            return file_code.text
        if self.proxy:
            return await get_file_url(file_code.code)
        else:
            async with self.session.client(
                    "s3",
                    endpoint_url=self.endpoint_url,
                    region_name=self.region_name,
                    config=Config(signature_version=self.signature_version),
            ) as s3:
                result = await s3.generate_presigned_url(
                    "get_object",
                    Params={
                        "Bucket": self.bucket_name,
                        "Key": await file_code.get_file_path(),
                    },
                    ExpiresIn=3600,
                )
                return result

    async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str):
        """
        保存分片到 S3(使用独立对象存储每个分片)
        注意:这里不使用 S3 原生的 multipart upload,而是将每个分片作为独立对象存储
        """
        chunk_key = str(Path(save_path).parent / "chunks" / upload_id / f"{chunk_index}.part")
        async with self.session.client(
            's3',
            endpoint_url=self.endpoint_url,
            aws_session_token=self.aws_session_token,
            region_name=self.region_name,
            config=Config(signature_version=self.signature_version),
        ) as s3:
            # 将分片作为独立对象上传
            await s3.put_object(
                Bucket=self.bucket_name,
                Key=chunk_key,
                Body=chunk_data,
                Metadata={
                    'chunk-hash': chunk_hash,
                    'chunk-index': str(chunk_index)
                }
            )

    async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]:
        """
        合并 S3 上的分片文件
        使用 S3 的 multipart upload API 实现流式合并,避免内存问题
        """
        file_sha256 = hashlib.sha256()
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)

        async with self.session.client(
            's3',
            endpoint_url=self.endpoint_url,
            aws_session_token=self.aws_session_token,
            region_name=self.region_name,
            config=Config(signature_version=self.signature_version),
        ) as s3:
            # 创建 multipart upload
            mpu = await s3.create_multipart_upload(
                Bucket=self.bucket_name,
                Key=save_path,
                ContentType='application/octet-stream'
            )
            mpu_id = mpu['UploadId']
            parts = []

            try:
                # 按顺序读取、验证并上传每个分片
                for i in range(chunk_info.total_chunks):
                    chunk_key = f"{chunk_dir}/{i}.part"
                    chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first()
                    if not chunk_record:
                        raise ValueError(f"分片{i}记录不存在")

                    try:
                        response = await s3.get_object(
                            Bucket=self.bucket_name,
                            Key=chunk_key
                        )
                        chunk_data = await response['Body'].read()
                    except Exception as e:
                        raise ValueError(f"分片{i}文件不存在: {e}")

                    current_hash = hashlib.sha256(chunk_data).hexdigest()
                    if current_hash != chunk_record.chunk_hash:
                        raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}")

                    file_sha256.update(chunk_data)

                    # 上传分片到 multipart upload
                    part_response = await s3.upload_part(
                        Bucket=self.bucket_name,
                        Key=save_path,
                        UploadId=mpu_id,
                        PartNumber=i + 1,  # S3 part numbers start at 1
                        Body=chunk_data
                    )
                    parts.append({
                        'PartNumber': i + 1,
                        'ETag': part_response['ETag']
                    })

                    # 释放内存
                    del chunk_data

                # 完成 multipart upload
                await s3.complete_multipart_upload(
                    Bucket=self.bucket_name,
                    Key=save_path,
                    UploadId=mpu_id,
                    MultipartUpload={'Parts': parts}
                )
            except Exception as e:
                # 出错时取消 multipart upload
                await s3.abort_multipart_upload(
                    Bucket=self.bucket_name,
                    Key=save_path,
                    UploadId=mpu_id
                )
                raise e

        return save_path, file_sha256.hexdigest()

    async def clean_chunks(self, upload_id: str, save_path: str):
        """
        清理 S3 上的临时分片文件
        :param upload_id: 上传会话ID
        :param save_path: 文件保存路径
        """
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)
        async with self.session.client(
            's3',
            endpoint_url=self.endpoint_url,
            aws_session_token=self.aws_session_token,
            region_name=self.region_name,
            config=Config(signature_version=self.signature_version),
        ) as s3:
            try:
                # 列出并删除所有分片对象
                paginator = s3.get_paginator('list_objects_v2')
                async for page in paginator.paginate(Bucket=self.bucket_name, Prefix=chunk_dir):
                    objects = page.get('Contents', [])
                    if objects:
                        delete_objects = [{'Key': obj['Key']} for obj in objects]
                        await s3.delete_objects(
                            Bucket=self.bucket_name,
                            Delete={'Objects': delete_objects}
                        )
            except Exception as e:
                logger.info(f"清理 S3 分片数据时出错: {e}")

    async def generate_presigned_upload_url(self, save_path: str, expires_in: int = 900) -> Optional[str]:
        """
        生成S3预签名上传URL
        :param save_path: 文件保存路径
        :param expires_in: URL过期时间(秒),默认15分钟
        :return: 预签名PUT URL
        """
        async with self.session.client(
            "s3",
            endpoint_url=self.endpoint_url,
            aws_session_token=self.aws_session_token,
            region_name=self.region_name,
            config=Config(signature_version=self.signature_version),
        ) as s3:
            return await s3.generate_presigned_url(
                "put_object",
                Params={
                    "Bucket": self.bucket_name,
                    "Key": save_path,
                },
                ExpiresIn=expires_in,
            )

    async def file_exists(self, save_path: str) -> bool:
        """
        检查文件是否存在于S3
        :param save_path: 文件路径
        :return: 文件是否存在
        """
        async with self.session.client(
            "s3",
            endpoint_url=self.endpoint_url,
            aws_session_token=self.aws_session_token,
            region_name=self.region_name,
            config=Config(signature_version=self.signature_version),
        ) as s3:
            try:
                await s3.head_object(Bucket=self.bucket_name, Key=save_path)
                return True
            except Exception:
                return False


class OneDriveFileStorage(FileStorageInterface):
    def __init__(self):
        try:
            import msal
            from office365.graph_client import GraphClient
            from office365.runtime.client_request_exception import (
                ClientRequestException,
            )
        except ImportError:
            raise ImportError("请先安装`msal`和`Office365-REST-Python-Client`")
        self.msal = msal
        self.domain = settings.onedrive_domain
        self.client_id = settings.onedrive_client_id
        self.username = settings.onedrive_username
        self.password = settings.onedrive_password
        self.proxy = settings.onedrive_proxy
        self._ClientRequestException = ClientRequestException

        try:
            client = GraphClient(self.acquire_token_pwd)
            self.root_path = (
                client.me.drive.root.get_by_path(settings.onedrive_root_path)
                .get()
                .execute_query()
            )
        except ClientRequestException as e:
            if e.code == "itemNotFound":
                client.me.drive.root.create_folder(settings.onedrive_root_path)
                self.root_path = (
                    client.me.drive.root.get_by_path(
                        settings.onedrive_root_path)
                    .get()
                    .execute_query()
                )
            else:
                raise e
        except Exception as e:
            raise Exception("OneDrive验证失败,请检查配置是否正确\n" + str(e))

    def acquire_token_pwd(self):
        authority_url = f"https://login.microsoftonline.com/{self.domain}"
        app = self.msal.PublicClientApplication(
            authority=authority_url, client_id=self.client_id
        )
        result = app.acquire_token_by_username_password(
            username=self.username,
            password=self.password,
            scopes=["https://graph.microsoft.com/.default"],
        )
        return result

    def _get_path_str(self, path):
        if isinstance(path, str):
            path = path.replace("\\", "/").replace("//", "/").split("/")
        elif isinstance(path, Path):
            path = str(path).replace("\\", "/").replace("//", "/").split("/")
        else:
            raise TypeError("path must be str or Path")
        path[-1] = path[-1].split(".")[0]
        return "/".join(path)

    def _save(self, file, save_path):
        content = file.file.read()
        name = save_path(file.filename)
        path = self._get_path_str(save_path)
        self.root_path.get_by_path(path).upload(name, content).execute_query()

    async def save_file(self, file: UploadFile, save_path: str):
        await asyncio.to_thread(self._save, file, save_path)

    def _delete(self, save_path):
        path = self._get_path_str(save_path)
        try:
            self.root_path.get_by_path(path).delete_object().execute_query()
        except self._ClientRequestException as e:
            if e.code == "itemNotFound":
                pass
            else:
                raise e

    async def delete_file(self, file_code: FileCodes):
        await asyncio.to_thread(self._delete, await file_code.get_file_path())

    def _convert_link_to_download_link(self, link):
        p1 = re.search(r"https://(.+)\.sharepoint\.com", link).group(1)
        p2 = re.search(r"personal/(.+)/", link).group(1)
        p3 = re.search(rf"{p2}/(.+)", link).group(1)
        return f"https://{p1}.sharepoint.com/personal/{p2}/_layouts/52/download.aspx?share={p3}"

    def _get_file_url(self, save_path, name):
        path = self._get_path_str(save_path)
        remote_file = self.root_path.get_by_path(path + "/" + name)
        expiration_datetime = datetime.datetime.now(
            tz=datetime.timezone.utc
        ) + datetime.timedelta(hours=1)
        expiration_datetime = expiration_datetime.strftime(
            "%Y-%m-%dT%H:%M:%SZ")
        permission = remote_file.create_link(
            "view", "anonymous", expiration_datetime=expiration_datetime
        ).execute_query()
        return self._convert_link_to_download_link(permission.link.webUrl)

    async def get_file_response(self, file_code: FileCodes):
        try:
            filename = file_code.prefix + file_code.suffix
            link = await asyncio.to_thread(
                self._get_file_url, await file_code.get_file_path(), filename
            )
            
            content_length = None  # 初始化为 None,表示未知大小
            
            # 创建ClientSession并复用
            session = aiohttp.ClientSession()
            
            # 尝试发送HEAD请求获取Content-Length
            try:
                async with session.head(link) as resp:
                    if resp.status == 200 and 'Content-Length' in resp.headers:
                        content_length = int(resp.headers['Content-Length'])
            except Exception:
                # 如果HEAD请求失败,则不提供 Content-Length
                pass
            
            async def stream_generator():
                try:
                    async with session.get(link) as resp:
                        if resp.status != 200:
                            raise HTTPException(
                                status_code=resp.status,
                                detail=f"从OneDrive获取文件失败: {resp.status}"
                            )
                        chunk_size = 65536
                        while True:
                            chunk = await resp.content.read(chunk_size)
                            if not chunk:
                                break
                            yield chunk
                finally:
                    await session.close()
            
            encoded_filename = quote(filename, safe='')
            headers = {
                "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
            }
            if content_length is not None:
                headers["Content-Length"] = str(content_length)
            return StreamingResponse(
                stream_generator(),
                media_type="application/octet-stream",
                headers=headers
            )
        except HTTPException:
            raise
        except Exception:
            raise HTTPException(status_code=503, detail="服务代理下载异常,请稍后再试")

    async def get_file_url(self, file_code: FileCodes):
        if self.proxy:
            return await get_file_url(file_code.code)
        else:
            return await asyncio.to_thread(
                self._get_file_url,
                await file_code.get_file_path(),
                f"{file_code.prefix}{file_code.suffix}",
            )

    def _save_chunk(self, chunk_path: str, chunk_data: bytes):
        """同步保存分片到 OneDrive"""
        path_parts = chunk_path.replace("\\", "/").split("/")
        filename = path_parts[-1]
        dir_path = "/".join(path_parts[:-1])
        
        # 确保目录存在
        current_folder = self.root_path
        for part in dir_path.split("/"):
            if part:
                try:
                    current_folder = current_folder.get_by_path(part).get().execute_query()
                except self._ClientRequestException as e:
                    if e.code == "itemNotFound":
                        current_folder = current_folder.create_folder(part).execute_query()
                    else:
                        raise e
        
        # 上传分片
        current_folder.upload(filename, chunk_data).execute_query()

    async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str):
        """保存分片到 OneDrive"""
        chunk_path = str(Path(save_path).parent / "chunks" / upload_id / f"{chunk_index}.part")
        await asyncio.to_thread(self._save_chunk, chunk_path, chunk_data)

    def _read_chunk(self, chunk_path: str) -> bytes:
        """同步读取分片"""
        path = self._get_path_str(chunk_path)
        file_obj = self.root_path.get_by_path(path).get().execute_query()
        return file_obj.get_content().execute_query().value

    def _upload_merged(self, save_path: str, data: bytes):
        """同步上传合并后的文件"""
        path_parts = save_path.replace("\\", "/").split("/")
        filename = path_parts[-1]
        dir_path = "/".join(path_parts[:-1])
        
        # 确保目录存在
        current_folder = self.root_path
        for part in dir_path.split("/"):
            if part:
                try:
                    current_folder = current_folder.get_by_path(part).get().execute_query()
                except self._ClientRequestException as e:
                    if e.code == "itemNotFound":
                        current_folder = current_folder.create_folder(part).execute_query()
                    else:
                        raise e
        
        current_folder.upload(filename, data).execute_query()

    async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]:
        """合并 OneDrive 上的分片文件,使用临时文件避免内存问题"""
        file_sha256 = hashlib.sha256()
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)

        # 使用临时文件存储合并数据
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_path = temp_file.name

        try:
            async with aiofiles.open(temp_path, 'wb') as out_file:
                for i in range(chunk_info.total_chunks):
                    chunk_path = f"{chunk_dir}/{i}.part"
                    chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first()
                    if not chunk_record:
                        raise ValueError(f"分片{i}记录不存在")

                    try:
                        chunk_data = await asyncio.to_thread(self._read_chunk, chunk_path)
                    except Exception as e:
                        raise ValueError(f"分片{i}文件不存在: {e}")

                    current_hash = hashlib.sha256(chunk_data).hexdigest()
                    if current_hash != chunk_record.chunk_hash:
                        raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}")

                    file_sha256.update(chunk_data)
                    await out_file.write(chunk_data)
                    del chunk_data  # 释放内存

            # 读取临时文件并上传
            async with aiofiles.open(temp_path, 'rb') as f:
                merged_content = await f.read()
            await asyncio.to_thread(self._upload_merged, save_path, merged_content)
        finally:
            # 清理临时文件
            if os.path.exists(temp_path):
                os.unlink(temp_path)

        return save_path, file_sha256.hexdigest()

    def _delete_chunk_dir(self, chunk_dir: str):
        """同步删除分片目录"""
        try:
            path = self._get_path_str(chunk_dir)
            self.root_path.get_by_path(path).delete_object().execute_query()
        except self._ClientRequestException as e:
            if e.code != "itemNotFound":
                raise e

    async def clean_chunks(self, upload_id: str, save_path: str):
        """清理 OneDrive 上的临时分片文件"""
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)
        try:
            await asyncio.to_thread(self._delete_chunk_dir, chunk_dir)
        except Exception as e:
            logger.info(f"清理 OneDrive 分片时出错: {e}")

    def _file_exists(self, save_path: str) -> bool:
        """同步检查文件是否存在"""
        try:
            path = self._get_path_str(save_path)
            self.root_path.get_by_path(path).get().execute_query()
            return True
        except self._ClientRequestException as e:
            if e.code == "itemNotFound":
                return False
            raise e

    async def file_exists(self, save_path: str) -> bool:
        """
        检查文件是否存在于OneDrive
        :param save_path: 文件路径
        :return: 文件是否存在
        """
        return await asyncio.to_thread(self._file_exists, save_path)


class OpenDALFileStorage(FileStorageInterface):
    def __init__(self):
        try:
            import opendal
        except ImportError:
            raise ImportError('请先安装 `opendal`, 例如: "pip install opendal"')
        self.service = settings.opendal_scheme
        service_settings = {}
        for key, value in settings.items():
            if key.startswith("opendal_" + self.service):
                setting_name = key.split("_", 2)[2]
                service_settings[setting_name] = value
        self.operator = opendal.AsyncOperator(
            settings.opendal_scheme, **service_settings
        )

    async def save_file(self, file: UploadFile, save_path: str):
        # 使用 asyncio.to_thread 避免阻塞事件循环
        content = await asyncio.to_thread(file.file.read)
        await self.operator.write(save_path, content)

    async def delete_file(self, file_code: FileCodes):
        await self.operator.delete(await file_code.get_file_path())

    async def get_file_url(self, file_code: FileCodes):
        return await get_file_url(file_code.code)

    async def get_file_response(self, file_code: FileCodes):
        try:
            filename = file_code.prefix + file_code.suffix
            content_length = None  # 初始化为 None,表示未知大小
            
            # 尝试获取文件大小
            try:
                stat_result = await self.operator.stat(await file_code.get_file_path())
                if hasattr(stat_result, 'content_length') and stat_result.content_length:
                    content_length = stat_result.content_length
                elif hasattr(stat_result, 'size') and stat_result.size:
                    content_length = stat_result.size
            except Exception:
                # 如果获取大小失败,则不提供 Content-Length
                pass
            
            # 尝试使用流式读取器
            try:
                # OpenDAL 可能提供 reader 方法返回一个异步读取器
                reader = await self.operator.reader(await file_code.get_file_path())
            except AttributeError:
                # 如果 reader 方法不存在,回退到全量读取(兼容旧版本)
                content = await self.operator.read(await file_code.get_file_path())
                encoded_filename = quote(filename, safe='')
                headers = {
                    "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
                }
                if content_length is not None:
                    headers["Content-Length"] = str(content_length)
                return Response(
                    content, headers=headers, media_type="application/octet-stream"
                )
            
            async def stream_generator():
                chunk_size = 65536
                while True:
                    chunk = await reader.read(chunk_size)
                    if not chunk:
                        break
                    yield chunk
            
            encoded_filename = quote(filename, safe='')
            headers = {
                "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
            }
            if content_length is not None:
                headers["Content-Length"] = str(content_length)
            return StreamingResponse(
                stream_generator(),
                media_type="application/octet-stream",
                headers=headers
            )
        except Exception as e:
            logger.info(e)
            raise HTTPException(status_code=404, detail="文件已过期删除")

    async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str):
        """保存分片到 OpenDAL 存储"""
        chunk_path = str(Path(save_path).parent / "chunks" / upload_id / f"{chunk_index}.part")
        await self.operator.write(chunk_path, chunk_data)

    async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]:
        """合并 OpenDAL 存储上的分片文件,使用临时文件避免内存问题"""
        file_sha256 = hashlib.sha256()
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)

        # 使用临时文件存储合并数据
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_path = temp_file.name

        try:
            async with aiofiles.open(temp_path, 'wb') as out_file:
                for i in range(chunk_info.total_chunks):
                    chunk_path = f"{chunk_dir}/{i}.part"
                    chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first()
                    if not chunk_record:
                        raise ValueError(f"分片{i}记录不存在")

                    try:
                        chunk_data = await self.operator.read(chunk_path)
                    except Exception as e:
                        raise ValueError(f"分片{i}文件不存在: {e}")

                    current_hash = hashlib.sha256(chunk_data).hexdigest()
                    if current_hash != chunk_record.chunk_hash:
                        raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}")

                    file_sha256.update(chunk_data)
                    await out_file.write(chunk_data)
                    del chunk_data  # 释放内存

            # 读取临时文件并写入存储
            async with aiofiles.open(temp_path, 'rb') as f:
                merged_content = await f.read()
            await self.operator.write(save_path, merged_content)
        finally:
            # 清理临时文件
            if os.path.exists(temp_path):
                os.unlink(temp_path)
        
        return save_path, file_sha256.hexdigest()

    async def clean_chunks(self, upload_id: str, save_path: str):
        """清理 OpenDAL 存储上的临时分片文件"""
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)
        try:
            # OpenDAL 支持递归删除
            await self.operator.remove_all(chunk_dir)
        except Exception as e:
            logger.info(f"清理 OpenDAL 分片时出错: {e}")

    async def file_exists(self, save_path: str) -> bool:
        """
        检查文件是否存在于OpenDAL存储
        :param save_path: 文件路径
        :return: 文件是否存在
        """
        try:
            await self.operator.stat(save_path)
            return True
        except Exception:
            return False


class WebDAVFileStorage(FileStorageInterface):
    _instance: Optional["WebDAVFileStorage"] = None

    def __init__(self):
        if not hasattr(self, "_initialized"):
            self.base_url = settings.webdav_url.rstrip("/") + "/"
            self.auth = aiohttp.BasicAuth(
                login=settings.webdav_username, password=settings.webdav_password
            )
            self._initialized = True

    def _build_url(self, path: str) -> str:
        encoded_path = quote(str(path.replace("\\", "/").lstrip("/")).lstrip("/"))
        return f"{self.base_url}{encoded_path}"

    async def _mkdir_p(self, directory_path: str):
        """递归创建目录(类似mkdir -p)"""
        path_obj = Path(unquote(directory_path))
        current_path = ""

        async with aiohttp.ClientSession(auth=self.auth) as session:
            # 逐级检查目录是否存在
            for part in path_obj.parts:
                current_path = str(Path(current_path) / part)
                url = self._build_url(current_path)

                # 检查目录是否存在
                async with session.head(url) as resp:
                    if resp.status == 404:
                        # 创建目录
                        async with session.request("MKCOL", url) as mkcol_resp:
                            if mkcol_resp.status not in (200, 201, 409):
                                content = await mkcol_resp.text()
                                raise HTTPException(
                                    status_code=mkcol_resp.status,
                                    detail=f"目录创建失败: {content[:200]}",
                                )

    async def _is_dir_empty(self, dir_path: str) -> bool:
        """检查目录是否为空"""
        url = self._build_url(dir_path)

        async with aiohttp.ClientSession(auth=self.auth) as session:
            async with session.request("PROPFIND", url, headers={"Depth": "1"}) as resp:
                if resp.status != 207:  # 207 是 Multi-Status 响应
                    return False
                content = await resp.text()
                # 如果只有一个 response(当前目录),说明目录为空
                return content.count("<D:response>") <= 1

    async def _delete_empty_dirs(self, file_path: str, session: aiohttp.ClientSession):
        """递归删除空目录"""
        path_obj = Path(file_path)
        current_path = path_obj.parent

        while str(current_path) != ".":
            if not await self._is_dir_empty(str(current_path)):
                break

            url = self._build_url(str(current_path))
            async with session.delete(url) as resp:
                if resp.status not in (200, 204, 404):
                    break

            current_path = current_path.parent

    async def save_file(self, file: UploadFile, save_path: str):
        """保存文件(自动创建目录,流式上传)"""
        path_obj = Path(save_path)
        directory_path = str(path_obj.parent)
        # 提取原始文件名并进行清理
        filename = await sanitize_filename(path_obj.name)
        # 构建安全的保存路径
        safe_save_path = str(Path(directory_path) / filename)

        try:
            # 先创建目录结构
            await self._mkdir_p(directory_path)
            # 上传文件(流式)
            url = self._build_url(safe_save_path)

            async def file_sender():
                """流式读取文件内容"""
                chunk_size = 256 * 1024  # 256KB chunks
                while True:
                    chunk = await asyncio.to_thread(file.file.read, chunk_size)
                    if not chunk:
                        break
                    yield chunk

            async with aiohttp.ClientSession(auth=self.auth) as session:
                async with session.put(
                        url,
                        data=file_sender(),
                        headers={"Content-Type": file.content_type or "application/octet-stream"}
                ) as resp:
                    if resp.status not in (200, 201, 204):
                        content = await resp.text()
                        raise HTTPException(
                            status_code=resp.status,
                            detail=f"文件上传失败: {content[:200]}",
                        )
        except aiohttp.ClientError as e:
            raise HTTPException(
                status_code=503, detail=f"WebDAV连接异常: {str(e)}")

    async def delete_file(self, file_code: FileCodes):
        """删除WebDAV文件及空目录"""
        file_path = await file_code.get_file_path()
        url = self._build_url(file_path)
        try:
            async with aiohttp.ClientSession(auth=self.auth) as session:
                # 删除文件
                async with session.delete(url) as resp:
                    if resp.status not in (200, 204, 404):
                        content = await resp.text()
                        raise HTTPException(
                            status_code=resp.status,
                            detail=f"WebDAV删除失败: {content[:200]}",
                        )

                # 使用同一个 session 删除空目录
                await self._delete_empty_dirs(file_path, session)

        except aiohttp.ClientError as e:
            raise HTTPException(
                status_code=503, detail=f"WebDAV连接异常: {str(e)}")

    async def get_file_url(self, file_code: FileCodes):
        return await get_file_url(file_code.code)

    async def get_file_response(self, file_code: FileCodes):
        """获取文件响应(代理模式)"""
        try:
            filename = file_code.prefix + file_code.suffix
            url = self._build_url(await file_code.get_file_path())
            content_length = None  # 初始化为 None,表示未知大小
            
            # 创建ClientSession并复用(包含认证头)
            session = aiohttp.ClientSession(headers={
                "Authorization": f"Basic {base64.b64encode(f'{settings.webdav_username}:{settings.webdav_password}'.encode()).decode()}"
            })
            
            # 尝试发送HEAD请求获取Content-Length
            try:
                async with session.head(url) as resp:
                    if resp.status == 200 and 'Content-Length' in resp.headers:
                        content_length = int(resp.headers['Content-Length'])
            except Exception:
                # 如果HEAD请求失败,则不提供 Content-Length
                pass
            
            async def stream_generator():
                try:
                    async with session.get(url) as resp:
                        if resp.status != 200:
                            raise HTTPException(
                                status_code=resp.status,
                                detail=f"文件获取失败{resp.status}: {await resp.text()}",
                            )
                        chunk_size = 65536
                        while True:
                            chunk = await resp.content.read(chunk_size)
                            if not chunk:
                                break
                            yield chunk
                finally:
                    await session.close()
            
            encoded_filename = quote(filename, safe='')
            headers = {
                "Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}"
            }
            if content_length is not None:
                headers["Content-Length"] = str(content_length)
            return StreamingResponse(
                stream_generator(),
                media_type="application/octet-stream",
                headers=headers
            )
        except aiohttp.ClientError as e:
            raise HTTPException(
                status_code=503, detail=f"WebDAV连接异常: {str(e)}")

    async def save_chunk(self, upload_id: str, chunk_index: int, chunk_data: bytes, chunk_hash: str, save_path: str):
        """保存分片到 WebDAV"""
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)
        chunk_path = f"{chunk_dir}/{chunk_index}.part"
        
        # 先创建目录结构
        await self._mkdir_p(chunk_dir)
        
        chunk_url = self._build_url(chunk_path)
        async with aiohttp.ClientSession(auth=self.auth) as session:
            async with session.put(chunk_url, data=chunk_data) as resp:
                if resp.status not in (200, 201, 204):
                    content = await resp.text()
                    raise HTTPException(
                        status_code=resp.status,
                        detail=f"分片上传失败: {content[:200]}"
                    )

    async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, save_path: str) -> tuple[str, str]:
        """
        合并 WebDAV 上的分片文件
        使用临时文件避免内存问题
        """
        file_sha256 = hashlib.sha256()
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)

        # 使用临时文件存储合并数据,避免内存问题
        with tempfile.NamedTemporaryFile(delete=False) as temp_file:
            temp_path = temp_file.name

        try:
            async with aiohttp.ClientSession(auth=self.auth) as session:
                # 按顺序读取并验证每个分片,写入临时文件
                async with aiofiles.open(temp_path, 'wb') as out_file:
                    for i in range(chunk_info.total_chunks):
                        chunk_path = f"{chunk_dir}/{i}.part"
                        chunk_url = self._build_url(chunk_path)

                        # 获取分片记录
                        chunk_record = await UploadChunk.filter(upload_id=upload_id, chunk_index=i).first()
                        if not chunk_record:
                            raise ValueError(f"分片{i}记录不存在")

                        # 下载分片数据
                        async with session.get(chunk_url) as resp:
                            if resp.status != 200:
                                raise ValueError(f"分片{i}文件不存在或无法访问")
                            chunk_data = await resp.read()

                        # 验证哈希
                        current_hash = hashlib.sha256(chunk_data).hexdigest()
                        if current_hash != chunk_record.chunk_hash:
                            raise ValueError(f"分片{i}哈希不匹配: 期望 {chunk_record.chunk_hash}, 实际 {current_hash}")

                        file_sha256.update(chunk_data)
                        await out_file.write(chunk_data)
                        del chunk_data  # 释放内存

                # 确保目标目录存在
                output_dir = str(Path(save_path).parent)
                await self._mkdir_p(output_dir)

                # 流式上传合并后的文件
                output_url = self._build_url(save_path)

                async def file_sender():
                    async with aiofiles.open(temp_path, 'rb') as f:
                        while True:
                            chunk = await f.read(256 * 1024)
                            if not chunk:
                                break
                            yield chunk

                async with session.put(output_url, data=file_sender()) as resp:
                    if resp.status not in (200, 201, 204):
                        content = await resp.text()
                        raise HTTPException(
                            status_code=resp.status,
                            detail=f"合并文件上传失败: {content[:200]}"
                        )
        finally:
            # 清理临时文件
            if os.path.exists(temp_path):
                os.unlink(temp_path)

        return save_path, file_sha256.hexdigest()

    async def clean_chunks(self, upload_id: str, save_path: str):
        """
        清理 WebDAV 上的临时分片文件
        :param upload_id: 上传会话ID
        :param save_path: 文件保存路径
        """
        chunk_dir = str(Path(save_path).parent / "chunks" / upload_id)
        chunk_dir_url = self._build_url(chunk_dir)
        async with aiohttp.ClientSession(auth=self.auth) as session:
            try:
                # 检查分片目录是否存在
                async with session.request("PROPFIND", chunk_dir_url, headers={"Depth": "1"}) as resp:
                    if resp.status == 207:  # 207 表示 Multi-Status
                        # 获取目录下的所有分片文件
                        xml_data = await resp.text()
                        file_paths = re.findall(
                            r'<D:href>(.*?)</D:href>', xml_data)
                        for file_path in file_paths:
                            if file_path.endswith(".part"):
                                # 删除分片文件
                                file_url = self._build_url(file_path)
                                async with session.delete(file_url) as delete_resp:
                                    if delete_resp.status not in (200, 204, 404):
                                        logger.info(f"删除分片文件失败: {file_path}")

                        # 删除分片目录
                        async with session.delete(chunk_dir_url) as delete_resp:
                            if delete_resp.status not in (200, 204, 404):
                                logger.info(f"删除分片目录失败: {chunk_dir_url}")
                    else:
                        logger.info(f"分片目录不存在: {chunk_dir_url}")
            except Exception as e:
                logger.info(f"清理 WebDAV 分片时出错: {e}")

    async def file_exists(self, save_path: str) -> bool:
        """
        检查文件是否存在于WebDAV
        :param save_path: 文件路径
        :return: 文件是否存在
        """
        url = self._build_url(save_path)
        async with aiohttp.ClientSession(auth=self.auth) as session:
            async with session.head(url) as resp:
                return resp.status == 200


storages = {
    "local": SystemFileStorage,
    "s3": S3FileStorage,
    "onedrive": OneDriveFileStorage,
    "opendal": OpenDALFileStorage,
    "webdav": WebDAVFileStorage,
}


================================================
FILE: core/tasks.py
================================================
# @Time    : 2023/8/15 22:00
# @Author  : Lan
# @File    : tasks.py
# @Software: PyCharm
import asyncio
import datetime
import logging
import os

from tortoise.expressions import Q

from apps.base.models import FileCodes, UploadChunk
from apps.base.utils import ip_limit, get_chunk_file_path_name
from core.config import refresh_settings
from core.settings import settings, data_root
from core.storage import FileStorageInterface, storages
from core.utils import get_now


async def delete_expire_files():
    while True:
        try:
            await refresh_settings()
            file_storage: FileStorageInterface = storages[settings.file_storage]()
            # 遍历 share目录下的所有文件夹,删除空的文件夹,并判断父目录是否为空,如果为空也删除
            if settings.file_storage == "local":
                for root, dirs, files in os.walk(f"{data_root}/share/data"):
                    if not dirs and not files:
                        os.rmdir(root)
            await ip_limit["error"].remove_expired_ip()
            await ip_limit["upload"].remove_expired_ip()
            expire_data = await FileCodes.filter(
                Q(expired_at__lt=await get_now()) | Q(expired_count=0)
            ).all()
            for exp in expire_data:
                try:
                    await file_storage.delete_file(exp)
                except Exception as e:
                    logging.error(f"删除过期文件失败 code={exp.code}: {e}")
                try:
                    await exp.delete()
                except Exception as e:
                    logging.error(f"删除记录失败 code={exp.code}: {e}")
        except Exception as e:
            logging.error(e)
        finally:
            await asyncio.sleep(600)


async def clean_incomplete_uploads():
    while True:
        try:
            await refresh_settings()
            file_storage: FileStorageInterface = storages[settings.file_storage]()
            expire_hours = getattr(settings, "chunk_expire_hours", 24)
            now = await get_now()
            expire_time = now - datetime.timedelta(hours=expire_hours)
            expired_sessions = await UploadChunk.filter(
                chunk_index=-1, created_at__lt=expire_time
            ).all()

            for session in expired_sessions:
                try:
                    save_path = session.save_path
                    if not save_path:
                        _, _, _, _, save_path = await get_chunk_file_path_name(
                            session.file_name, session.upload_id
                        )
                    await file_storage.clean_chunks(session.upload_id, save_path)
                except Exception as e:
                    logging.error(
                        f"清理分片文件失败 upload_id={session.upload_id}: {e}"
                    )

                try:
                    await UploadChunk.filter(upload_id=session.upload_id).delete()
                    logging.info(f"已清理过期上传会话 upload_id={session.upload_id}")
                except Exception as e:
                    logging.error(
                        f"删除分片记录失败 upload_id={session.upload_id}: {e}"
                    )

        except Exception as e:
            logging.error(f"清理未完成上传任务异常: {e}")
        finally:
            await asyncio.sleep(3600)


================================================
FILE: core/utils.py
================================================
# @Time    : 2023/8/13 19:54
# @Author  : Lan
# @File    : utils.py
# @Software: PyCharm
import datetime
import hashlib
import os
import random
import re
import string
import time

from core.settings import settings


async def get_random_num():
    """
    获取随机数
    :return:
    """
    return random.randint(10000, 99999)


r_s = string.ascii_uppercase + string.digits


async def get_random_string():
    """
    获取随机字符串
    :return:
    """
    return "".join(random.choice(r_s) for _ in range(5))


async def get_now():
    """
    获取当前时间
    :return:
    """
    return datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8)))


async def get_select_token(code: str):
    """
    获取下载token
    :param code:
    :return:
    """
    token = settings.admin_token
    return hashlib.sha256(
        f"{code}{int(time.time() / 1000)}000{token}".encode()
    ).hexdigest()


async def get_file_url(code: str):
    """
    对于需要通过服务器中转下载的服务,获取文件下载地址
    :param code:
    :return:
    """
    return f"/share/download?key={await get_select_token(code)}&code={code}"


async def max_save_times_desc(max_save_seconds: int):
    """
    获取最大保存时间的描述
    :param max_save_seconds:
    :return:
    """

    def gen_desc_zh(value: int, desc: str):
        if value > 0:
            return f"{value}{desc}"
        else:
            return ""

    def gen_desc_en(value: int, desc: str):
        if value > 0:
            ret = f"{value} {desc}"
            if value > 1:
                ret += "s"
            ret += " "
            return ret
        else:
            return ""

    max_timedelta = datetime.timedelta(seconds=max_save_seconds)
    desc_zh, desc_en = "最长保存时间:", "Max save time: "
    desc_zh += gen_desc_zh(max_timedelta.days, "天")
    desc_en += gen_desc_en(max_timedelta.days, "day")
    desc_zh += gen_desc_zh(max_timedelta.seconds // 3600, "小时")
    desc_en += gen_desc_en(max_timedelta.seconds // 3600, "hour")
    desc_zh += gen_desc_zh(max_timedelta.seconds % 3600 // 60, "分钟")
    desc_en += gen_desc_en(max_timedelta.seconds % 3600 // 60, "minute")
    desc_zh += gen_desc_zh(max_timedelta.seconds % 60, "秒")
    desc_en += gen_desc_en(max_timedelta.seconds % 60, "second")
    return desc_zh, desc_en


def hash_password(password: str) -> str:
    """
    使用 SHA256 + salt 哈希密码
    返回格式: sha256$<salt>$<hash>
    """
    salt = os.urandom(16).hex()
    password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
    return f"sha256${salt}${password_hash}"


def verify_password(password: str, hashed: str) -> bool:
    """
    验证密码是否匹配
    支持新格式 (sha256$salt$hash) 和旧格式 (明文)
    """
    if not hashed:
        return False

    # 新格式: sha256$salt$hash
    if hashed.startswith("sha256$"):
        parts = hashed.split("$")
        if len(parts) != 3:
            return False
        _, salt, stored_hash = parts
        password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
        return password_hash == stored_hash

    # 旧格式: 明文比较 (兼容迁移前的数据)
    return password == hashed


def is_password_hashed(password: str) -> bool:
    """
    检查密码是否已经是哈希格式
    """
    return password.startswith("sha256$") and len(password.split("$")) == 3


async def sanitize_filename(filename: str) -> str:
    """
    安全处理文件名:
    1. 剥离路径只保留文件名
    2. 替换非法字符
    3. 处理空文件名情况
    """
    filename = os.path.basename(filename)
    illegal_chars = r'[\\/*?:"<>|\x00-\x1F]'  # 包含控制字符
    # 替换非法字符为下划线
    cleaned = re.sub(illegal_chars, "_", filename)
    # 处理空格(可选替换为_)
    cleaned = cleaned.replace(" ", "_")
    # 处理连续下划线
    cleaned = re.sub(r"_+", "_", cleaned)
    # 处理首尾特殊字符
    cleaned = cleaned.strip("._")
    # 处理空文件名情况
    if not cleaned:
        cleaned = "unnamed_file"
    # 长度限制(按需调整)
    return cleaned[:255]


================================================
FILE: docker-compose.yml
================================================
version: "3"
services:
  file-code-box:
    image: lanol/filecodebox:latest
    volumes:
      - fcb-data:/app/data:rw
    restart: unless-stopped
    ports:
      - "12345:12345"
volumes:
  fcb-data:
    external: false

================================================
FILE: docs/.vitepress/cache/deps/_metadata.json
================================================
{
  "hash": "8f855eaf",
  "configHash": "1b3ca22f",
  "lockfileHash": "bd28b2c2",
  "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": "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": "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": "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": "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": "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": "cc176c9c",
      "needsInterop": false
    }
  },
  "chunks": {
    "chunk-KT7LHMJ2": {
      "file": "chunk-KT7LHMJ2.js"
    },
    "chunk-CQOUZRMK": {
      "file": "chunk-CQOUZRMK.js"
    }
  }
}

================================================
FILE: docs/.vitepress/cache/deps/chunk-CQOUZRMK.js
================================================
// node_modules/.pnpm/@vue+shared@3.5.13/node_modules/@vue/shared/dist/shared.esm-bundler.js
function makeMap(str) {
  const map2 = /* @__PURE__ */ Object.create(null);
  for (const key of str.split(",")) map2[key] = 1;
  return (val) => val in map2;
}
var EMPTY_OBJ = true ? Object.freeze({}) : {};
var EMPTY_ARR = true ? Object.freeze([]) : [];
var NOOP = () => {
};
var NO = () => false;
var isOn = (key) => key.charCodeAt(0) === 111 && key.charCodeAt(1) === 110 && // uppercase letter
(key.charCodeAt(2) > 122 || key.charCodeAt(2) < 97);
var isModelListener = (key) => key.startsWith("onUpdate:");
var extend = Object.assign;
var remove = (arr, el) => {
  const i = arr.indexOf(el);
  if (i > -1) {
    arr.splice(i, 1);
  }
};
var hasOwnProperty = Object.prototype.hasOwnProperty;
var hasOwn = (val, key) => hasOwnProperty.call(val, key);
var isArray = Array.isArray;
var isMap = (val) => toTypeString(val) === "[object Map]";
var isSet = (val) => toTypeString(val) === "[object Set]";
var isDate = (val) => toTypeString(val) === "[object Date]";
var isRegExp = (val) => toTypeString(val) === "[object RegExp]";
var isFunction = (val) => typeof val === "function";
var isString = (val) => typeof val === "string";
var isSymbol = (val) => typeof val === "symbol";
var isObject = (val) => val !== null && typeof val === "object";
var isPromise = (val) => {
  return (isObject(val) || isFunction(val)) && isFunction(val.then) && isFunction(val.catch);
};
var objectToString = Object.prototype.toString;
var toTypeString = (value) => objectToString.call(value);
var toRawType = (value) => {
  return toTypeString(value).slice(8, -1);
};
var isPlainObject = (val) => toTypeString(val) === "[object Object]";
var isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key;
var isReservedProp = makeMap(
  // the leading comma is intentional so empty string "" is also included
  ",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"
);
var isBuiltInDirective = makeMap(
  "bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo"
);
var cacheStringFunction = (fn) => {
  const cache = /* @__PURE__ */ Object.create(null);
  return (str) => {
    const hit = cache[str];
    return hit || (cache[str] = fn(str));
  };
};
var camelizeRE = /-(\w)/g;
var camelize = cacheStringFunction(
  (str) => {
    return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : "");
  }
);
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cacheStringFunction(
  (str) => str.replace(hyphenateRE, "-$1").toLowerCase()
);
var capitalize = cacheStringFunction((str) => {
  return str.charAt(0).toUpperCase() + str.slice(1);
});
var toHandlerKey = cacheStringFunction(
  (str) => {
    const s = str ? `on${capitalize(str)}` : ``;
    return s;
  }
);
var hasChanged = (value, oldValue) => !Object.is(value, oldValue);
var invokeArrayFns = (fns, ...arg) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](...arg);
  }
};
var def = (obj, key, value, writable = false) => {
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: false,
    writable,
    value
  });
};
var looseToNumber = (val) => {
  const n = parseFloat(val);
  return isNaN(n) ? val : n;
};
var toNumber = (val) => {
  const n = isString(val) ? Number(val) : NaN;
  return isNaN(n) ? val : n;
};
var _globalThis;
var getGlobalThis = () => {
  return _globalThis || (_globalThis = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : typeof global !== "undefined" ? global : {});
};
var GLOBALS_ALLOWED = "Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol";
var isGloballyAllowed = makeMap(GLOBALS_ALLOWED);
function normalizeStyle(value) {
  if (isArray(value)) {
    const res = {};
    for (let i = 0; i < value.length; i++) {
      const item = value[i];
      const normalized = isString(item) ? parseStringStyle(item) : normalizeStyle(item);
      if (normalized) {
        for (const key in normalized) {
          res[key] = normalized[key];
        }
      }
    }
    return res;
  } else if (isString(value) || isObject(value)) {
    return value;
  }
}
var listDelimiterRE = /;(?![^(]*\))/g;
var propertyDelimiterRE = /:([^]+)/;
var styleCommentRE = /\/\*[^]*?\*\//g;
function parseStringStyle(cssText) {
  const ret = {};
  cssText.replace(styleCommentRE, "").split(listDelimiterRE).forEach((item) => {
    if (item) {
      const tmp = item.split(propertyDelimiterRE);
      tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim());
    }
  });
  return ret;
}
function stringifyStyle(styles) {
  if (!styles) return "";
  if (isString(styles)) return styles;
  let ret = "";
  for (const key in styles) {
    const value = styles[key];
    if (isString(value) || typeof value === "number") {
      const normalizedKey = key.startsWith(`--`) ? key : hyphenate(key);
      ret += `${normalizedKey}:${value};`;
    }
  }
  return ret;
}
function normalizeClass(value) {
  let res = "";
  if (isString(value)) {
    res = value;
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      const normalized = normalizeClass(value[i]);
      if (normalized) {
        res += normalized + " ";
      }
    }
  } else if (isObject(value)) {
    for (const name in value) {
      if (value[name]) {
        res += name + " ";
      }
    }
  }
  return res.trim();
}
function normalizeProps(props) {
  if (!props) return null;
  let { class: klass, style } = props;
  if (klass && !isString(klass)) {
    props.class = normalizeClass(klass);
  }
  if (style) {
    props.style = normalizeStyle(style);
  }
  return props;
}
var HTML_TAGS = "html,body,base,head,link,meta,style,title,address,article,aside,footer,header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,summary,template,blockquote,iframe,tfoot";
var SVG_TAGS = "svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,defs,desc,discard,ellipse,feBlend,feColorMatrix,feComponentTransfer,feComposite,feConvolveMatrix,feDiffuseLighting,feDisplacementMap,feDistantLight,feDropShadow,feFlood,feFuncA,feFuncB,feFuncG,feFuncR,feGaussianBlur,feImage,feMerge,feMergeNode,feMorphology,feOffset,fePointLight,feSpecularLighting,feSpotLight,feTile,feTurbulence,filter,foreignObject,g,hatch,hatchpath,image,line,linearGradient,marker,mask,mesh,meshgradient,meshpatch,meshrow,metadata,mpath,path,pattern,polygon,polyline,radialGradient,rect,set,solidcolor,stop,switch,symbol,text,textPath,title,tspan,unknown,use,view";
var MATH_TAGS = "annotation,annotation-xml,maction,maligngroup,malignmark,math,menclose,merror,mfenced,mfrac,mfraction,mglyph,mi,mlabeledtr,mlongdiv,mmultiscripts,mn,mo,mover,mpadded,mphantom,mprescripts,mroot,mrow,ms,mscarries,mscarry,msgroup,msline,mspace,msqrt,msrow,mstack,mstyle,msub,msubsup,msup,mtable,mtd,mtext,mtr,munder,munderover,none,semantics";
var VOID_TAGS = "area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr";
var isHTMLTag = makeMap(HTML_TAGS);
var isSVGTag = makeMap(SVG_TAGS);
var isMathMLTag = makeMap(MATH_TAGS);
var isVoidTag = makeMap(VOID_TAGS);
var specialBooleanAttrs = `itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly`;
var isSpecialBooleanAttr = makeMap(specialBooleanAttrs);
var isBooleanAttr = makeMap(
  specialBooleanAttrs + `,async,autofocus,autoplay,controls,default,defer,disabled,hidden,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected`
);
function includeBooleanAttr(value) {
  return !!value || value === "";
}
var isKnownHtmlAttr = makeMap(
  `accept,accept-charset,accesskey,action,align,allow,alt,async,autocapitalize,autocomplete,autofocus,autoplay,background,bgcolor,border,buffered,capture,challenge,charset,checked,cite,class,code,codebase,color,cols,colspan,content,contenteditable,contextmenu,controls,coords,crossorigin,csp,data,datetime,decoding,default,defer,dir,dirname,disabled,download,draggable,dropzone,enctype,enterkeyhint,for,form,formaction,formenctype,formmethod,formnovalidate,formtarget,headers,height,hidden,high,href,hreflang,http-equiv,icon,id,importance,inert,integrity,ismap,itemprop,keytype,kind,label,lang,language,loading,list,loop,low,manifest,max,maxlength,minlength,media,min,multiple,muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,preload,radiogroup,readonly,referrerpolicy,rel,required,reversed,rows,rowspan,sandbox,scope,scoped,selected,shape,size,sizes,slot,span,spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,target,title,translate,type,usemap,value,width,wrap`
);
var isKnownSvgAttr = makeMap(
  `xmlns,accent-height,accumulate,additive,alignment-baseline,alphabetic,amplitude,arabic-form,ascent,attributeName,attributeType,azimuth,baseFrequency,baseline-shift,baseProfile,bbox,begin,bias,by,calcMode,cap-height,class,clip,clipPathUnits,clip-path,clip-rule,color,color-interpolation,color-interpolation-filters,color-profile,color-rendering,contentScriptType,contentStyleType,crossorigin,cursor,cx,cy,d,decelerate,descent,diffuseConstant,direction,display,divisor,dominant-baseline,dur,dx,dy,edgeMode,elevation,enable-background,end,exponent,fill,fill-opacity,fill-rule,filter,filterRes,filterUnits,flood-color,flood-opacity,font-family,font-size,font-size-adjust,font-stretch,font-style,font-variant,font-weight,format,from,fr,fx,fy,g1,g2,glyph-name,glyph-orientation-horizontal,glyph-orientation-vertical,glyphRef,gradientTransform,gradientUnits,hanging,height,href,hreflang,horiz-adv-x,horiz-origin-x,id,ideographic,image-rendering,in,in2,intercept,k,k1,k2,k3,k4,kernelMatrix,kernelUnitLength,kerning,keyPoints,keySplines,keyTimes,lang,lengthAdjust,letter-spacing,lighting-color,limitingConeAngle,local,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mask,maskContentUnits,maskUnits,mathematical,max,media,method,min,mode,name,numOctaves,offset,opacity,operator,order,orient,orientation,origin,overflow,overline-position,overline-thickness,panose-1,paint-order,path,pathLength,patternContentUnits,patternTransform,patternUnits,ping,pointer-events,points,pointsAtX,pointsAtY,pointsAtZ,preserveAlpha,preserveAspectRatio,primitiveUnits,r,radius,referrerPolicy,refX,refY,rel,rendering-intent,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,result,rotate,rx,ry,scale,seed,shape-rendering,slope,spacing,specularConstant,specularExponent,speed,spreadMethod,startOffset,stdDeviation,stemh,stemv,stitchTiles,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,string,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,style,surfaceScale,systemLanguage,tabindex,tableValues,target,targetX,targetY,text-anchor,text-decoration,text-rendering,textLength,to,transform,transform-origin,type,u1,u2,underline-position,underline-thickness,unicode,unicode-bidi,unicode-range,units-per-em,v-alphabetic,v-hanging,v-ideographic,v-mathematical,values,vector-effect,version,vert-adv-y,vert-origin-x,vert-origin-y,viewBox,viewTarget,visibility,width,widths,word-spacing,writing-mode,x,x-height,x1,x2,xChannelSelector,xlink:actuate,xlink:arcrole,xlink:href,xlink:role,xlink:show,xlink:title,xlink:type,xmlns:xlink,xml:base,xml:lang,xml:space,y,y1,y2,yChannelSelector,z,zoomAndPan`
);
var isKnownMathMLAttr = makeMap(
  `accent,accentunder,actiontype,align,alignmentscope,altimg,altimg-height,altimg-valign,altimg-width,alttext,bevelled,close,columnsalign,columnlines,columnspan,denomalign,depth,dir,display,displaystyle,encoding,equalcolumns,equalrows,fence,fontstyle,fontweight,form,frame,framespacing,groupalign,height,href,id,indentalign,indentalignfirst,indentalignlast,indentshift,indentshiftfirst,indentshiftlast,indextype,justify,largetop,largeop,lquote,lspace,mathbackground,mathcolor,mathsize,mathvariant,maxsize,minlabelspacing,mode,other,overflow,position,rowalign,rowlines,rowspan,rquote,rspace,scriptlevel,scriptminsize,scriptsizemultiplier,selection,separator,separators,shift,side,src,stackalign,stretchy,subscriptshift,superscriptshift,symmetric,voffset,width,widths,xlink:href,xlink:show,xlink:type,xmlns`
);
function isRenderableAttrValue(value) {
  if (value == null) {
    return false;
  }
  const type = typeof value;
  return type === "string" || type === "number" || type === "boolean";
}
var cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g;
function getEscapedCssVarName(key, doubleEscape) {
  return key.replace(
    cssVarNameEscapeSymbolsRE,
    (s) => doubleEscape ? s === '"' ? '\\\\\\"' : `\\\\${s}` : `\\${s}`
  );
}
function looseCompareArrays(a, b) {
  if (a.length !== b.length) return false;
  let equal = true;
  for (let i = 0; equal && i < a.length; i++) {
    equal = looseEqual(a[i], b[i]);
  }
  return equal;
}
function looseEqual(a, b) {
  if (a === b) return true;
  let aValidType = isDate(a);
  let bValidType = isDate(b);
  if (aValidType || bValidType) {
    return aValidType && bValidType ? a.getTime() === b.getTime() : false;
  }
  aValidType = isSymbol(a);
  bValidType = isSymbol(b);
  if (aValidType || bValidType) {
    return a === b;
  }
  aValidType = isArray(a);
  bValidType = isArray(b);
  if (aValidType || bValidType) {
    return aValidType && bValidType ? looseCompareArrays(a, b) : false;
  }
  aValidType = isObject(a);
  bValidType = isObject(b);
  if (aValidType || bValidType) {
    if (!aValidType || !bValidType) {
      return false;
    }
    const aKeysCount = Object.keys(a).length;
    const bKeysCount = Object.keys(b).length;
    if (aKeysCount !== bKeysCount) {
      return false;
    }
    for (const key in a) {
      const aHasKey = a.hasOwnProperty(key);
      const bHasKey = b.hasOwnProperty(key);
      if (aHasKey && !bHasKey || !aHasKey && bHasKey || !looseEqual(a[key], b[key])) {
        return false;
      }
    }
  }
  return String(a) === String(b);
}
function looseIndexOf(arr, val) {
  return arr.findIndex((item) => looseEqual(item, val));
}
var isRef = (val) => {
  return !!(val && val["__v_isRef"] === true);
};
var toDisplayString = (val) => {
  return isString(val) ? val : val == null ? "" : isArray(val) || isObject(val) && (val.toString === objectToString || !isFunction(val.toString)) ? isRef(val) ? toDisplayString(val.value) : JSON.stringify(val, replacer, 2) : String(val);
};
var replacer = (_key, val) => {
  if (isRef(val)) {
    return replacer(_key, val.value);
  } else if (isMap(val)) {
    return {
      [`Map(${val.size})`]: [...val.entries()].reduce(
        (entries, [key, val2], i) => {
          entries[stringifySymbol(key, i) + " =>"] = val2;
          return entries;
        },
        {}
      )
    };
  } else if (isSet(val)) {
    return {
      [`Set(${val.size})`]: [...val.values()].map((v) => stringifySymbol(v))
    };
  } else if (isSymbol(val)) {
    return stringifySymbol(val);
  } else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {
    return String(val);
  }
  return val;
};
var stringifySymbol = (v, i = "") => {
  var _a;
  return (
    // Symbol.description in es2019+ so we need to cast here to pass
    // the lib: es2016 check
    isSymbol(v) ? `Symbol(${(_a = v.description) != null ? _a : i})` : v
  );
};

// node_modules/.pnpm/@vue+reactivity@3.5.13/node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js
function warn(msg, ...args) {
  console.warn(`[Vue warn] ${msg}`, ...args);
}
var activeEffectScope;
var EffectScope = class {
  constructor(detached = false) {
    this.detached = detached;
    this._active = true;
    this.effects = [];
    this.cleanups = [];
    this._isPaused = false;
    this.parent = activeEffectScope;
    if (!detached && activeEffectScope) {
      this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
        this
      ) - 1;
    }
  }
  get active() {
    return this._active;
  }
  pause() {
    if (this._active) {
      this._isPaused = true;
      let i, l;
      if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i++) {
          this.scopes[i].pause();
        }
      }
      for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].pause();
      }
    }
  }
  /**
   * Resumes the effect scope, including all child scopes and effects.
   */
  resume() {
    if (this._active) {
      if (this._isPaused) {
        this._isPaused = false;
        let i, l;
        if (this.scopes) {
          for (i = 0, l = this.scopes.length; i < l; i++) {
            this.scopes[i].resume();
          }
        }
        for (i = 0, l = this.effects.length; i < l; i++) {
          this.effects[i].resume();
        }
      }
    }
  }
  run(fn) {
    if (this._active) {
      const currentEffectScope = activeEffectScope;
      try {
        activeEffectScope = this;
        return fn();
      } finally {
        activeEffectScope = currentEffectScope;
      }
    } else if (true) {
      warn(`cannot run an inactive effect scope.`);
    }
  }
  /**
   * This should only be called on non-detached scopes
   * @internal
   */
  on() {
    activeEffectScope = this;
  }
  /**
   * This should only be called on non-detached scopes
   * @internal
   */
  off() {
    activeEffectScope = this.parent;
  }
  stop(fromParent) {
    if (this._active) {
      this._active = false;
      let i, l;
      for (i = 0, l = this.effects.length; i < l; i++) {
        this.effects[i].stop();
      }
      this.effects.length = 0;
      for (i = 0, l = this.cleanups.length; i < l; i++) {
        this.cleanups[i]();
      }
      this.cleanups.length = 0;
      if (this.scopes) {
        for (i = 0, l = this.scopes.length; i < l; i++) {
          this.scopes[i].stop(true);
        }
        this.scopes.length = 0;
      }
      if (!this.detached && this.parent && !fromParent) {
        const last = this.parent.scopes.pop();
        if (last && last !== this) {
          this.parent.scopes[this.index] = last;
          last.index = this.index;
        }
      }
      this.parent = void 0;
    }
  }
};
function effectScope(detached) {
  return new EffectScope(detached);
}
function getCurrentScope() {
  return activeEffectScope;
}
function onScopeDispose(fn, failSilently = false) {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn);
  } else if (!failSilently) {
    warn(
      `onScopeDispose() is called when there is no active effect scope to be associated with.`
    );
  }
}
var activeSub;
var pausedQueueEffects = /* @__PURE__ */ new WeakSet();
var ReactiveEffect = class {
  constructor(fn) {
    this.fn = fn;
    this.deps = void 0;
    this.depsTail = void 0;
    this.flags = 1 | 4;
    this.next = void 0;
    this.cleanup = void 0;
    this.scheduler = void 0;
    if (activeEffectScope && activeEffectScope.active) {
      activeEffectScope.effects.push(this);
    }
  }
  pause() {
    this.flags |= 64;
  }
  resume() {
    if (this.flags & 64) {
      this.flags &= ~64;
      if (pausedQueueEffects.has(this)) {
        pausedQueueEffects.delete(this);
        this.trigger();
      }
    }
  }
  /**
   * @internal
   */
  notify() {
    if (this.flags & 2 && !(this.flags & 32)) {
      return;
    }
    if (!(this.flags & 8)) {
      batch(this);
    }
  }
  run() {
    if (!(this.flags & 1)) {
      return this.fn();
    }
    this.flags |= 2;
    cleanupEffect(this);
    prepareDeps(this);
    const prevEffect = activeSub;
    const prevShouldTrack = shouldTrack;
    activeSub = this;
    shouldTrack = true;
    try {
      return this.fn();
    } finally {
      if (activeSub !== this) {
        warn(
          "Active effect was not restored correctly - this is likely a Vue internal bug."
        );
      }
      cleanupDeps(this);
      activeSub = prevEffect;
      shouldTrack = prevShouldTrack;
      this.flags &= ~2;
    }
  }
  stop() {
    if (this.flags & 1) {
      for (let link = this.deps; link; link = link.nextDep) {
        removeSub(link);
      }
      this.deps = this.depsTail = void 0;
      cleanupEffect(this);
      this.onStop && this.onStop();
      this.flags &= ~1;
    }
  }
  trigger() {
    if (this.flags & 64) {
      pausedQueueEffects.add(this);
    } else if (this.scheduler) {
      this.scheduler();
    } else {
      this.runIfDirty();
    }
  }
  /**
   * @internal
   */
  runIfDirty() {
    if (isDirty(this)) {
      this.run();
    }
  }
  get dirty() {
    return isDirty(this);
  }
};
var batchDepth = 0;
var batchedSub;
var batchedComputed;
function batch(sub, isComputed = false) {
  sub.flags |= 8;
  if (isComputed) {
    sub.next = batchedComputed;
    batchedComputed = sub;
    return;
  }
  sub.next = batchedSub;
  batchedSub = sub;
}
function startBatch() {
  batchDepth++;
}
function endBatch() {
  if (--batchDepth > 0) {
    return;
  }
  if (batchedComputed) {
    let e = batchedComputed;
    batchedComputed = void 0;
    while (e) {
      const next = e.next;
      e.next = void 0;
      e.flags &= ~8;
      e = next;
    }
  }
  let error;
  while (batchedSub) {
    let e = batchedSub;
    batchedSub = void 0;
    while (e) {
      const next = e.next;
      e.next = void 0;
      e.flags &= ~8;
      if (e.flags & 1) {
        try {
          ;
          e.trigger();
        } catch (err) {
          if (!error) error = err;
        }
      }
      e = next;
    }
  }
  if (error) throw error;
}
function prepareDeps(sub) {
  for (let link = sub.deps; link; link = link.nextDep) {
    link.version = -1;
    link.prevActiveLink = link.dep.activeLink;
    link.dep.activeLink = link;
  }
}
function cleanupDeps(sub) {
  let head;
  let tail = sub.depsTail;
  let link = tail;
  while (link) {
    const prev = link.prevDep;
    if (link.version === -1) {
      if (link === tail) tail = prev;
      removeSub(link);
      removeDep(link);
    } else {
      head = link;
    }
    link.dep.activeLink = link.prevActiveLink;
    link.prevActiveLink = void 0;
    link = prev;
  }
  sub.deps = head;
  sub.depsTail = tail;
}
function isDirty(sub) {
  for (let link = sub.deps; link; link = link.nextDep) {
    if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) {
      return true;
    }
  }
  if (sub._dirty) {
    return true;
  }
  return false;
}
function refreshComputed(computed3) {
  if (computed3.flags & 4 && !(computed3.flags & 16)) {
    return;
  }
  computed3.flags &= ~16;
  if (computed3.globalVersion === globalVersion) {
    return;
  }
  computed3.globalVersion = globalVersion;
  const dep = computed3.dep;
  computed3.flags |= 2;
  if (dep.version > 0 && !computed3.isSSR && computed3.deps && !isDirty(computed3)) {
    computed3.flags &= ~2;
    return;
  }
  const prevSub = activeSub;
  const prevShouldTrack = shouldTrack;
  activeSub = computed3;
  shouldTrack = true;
  try {
    prepareDeps(computed3);
    const value = computed3.fn(computed3._value);
    if (dep.version === 0 || hasChanged(value, computed3._value)) {
      computed3._value = value;
      dep.version++;
    }
  } catch (err) {
    dep.version++;
    throw err;
  } finally {
    activeSub = prevSub;
    shouldTrack = prevShouldTrack;
    cleanupDeps(computed3);
    computed3.flags &= ~2;
  }
}
function removeSub(link, soft = false) {
  const { dep, prevSub, nextSub } = link;
  if (prevSub) {
    prevSub.nextSub = nextSub;
    link.prevSub = void 0;
  }
  if (nextSub) {
    nextSub.prevSub = prevSub;
    link.nextSub = void 0;
  }
  if (dep.subsHead === link) {
    dep.subsHead = nextSub;
  }
  if (dep.subs === link) {
    dep.subs = prevSub;
    if (!prevSub && dep.computed) {
      dep.computed.flags &= ~4;
      for (let l = dep.computed.deps; l; l = l.nextDep) {
        removeSub(l, true);
      }
    }
  }
  if (!soft && !--dep.sc && dep.map) {
    dep.map.delete(dep.key);
  }
}
function removeDep(link) {
  const { prevDep, nextDep } = link;
  if (prevDep) {
    prevDep.nextDep = nextDep;
    link.prevDep = void 0;
  }
  if (nextDep) {
    nextDep.prevDep = prevDep;
    link.nextDep = void 0;
  }
}
function effect(fn, options) {
  if (fn.effect instanceof ReactiveEffect) {
    fn = fn.effect.fn;
  }
  const e = new ReactiveEffect(fn);
  if (options) {
    extend(e, options);
  }
  try {
    e.run();
  } catch (err) {
    e.stop();
    throw err;
  }
  const runner = e.run.bind(e);
  runner.effect = e;
  return runner;
}
function stop(runner) {
  runner.effect.stop();
}
var shouldTrack = true;
var trackStack = [];
function pauseTracking() {
  trackStack.push(shouldTrack);
  shouldTrack = false;
}
function resetTracking() {
  const last = trackStack.pop();
  shouldTrack = last === void 0 ? true : last;
}
function cleanupEffect(e) {
  const { cleanup } = e;
  e.cleanup = void 0;
  if (cleanup) {
    const prevSub = activeSub;
    activeSub = void 0;
    try {
      cleanup();
    } finally {
      activeSub = prevSub;
    }
  }
}
var globalVersion = 0;
var Link = class {
  constructor(sub, dep) {
    this.sub = sub;
    this.dep = dep;
    this.version = dep.version;
    this.nextDep = this.prevDep = this.nextSub = this.prevSub = this.prevActiveLink = void 0;
  }
};
var Dep = class {
  constructor(computed3) {
    this.computed = computed3;
    this.version = 0;
    this.activeLink = void 0;
    this.subs = void 0;
    this.map = void 0;
    this.key = void 0;
    this.sc = 0;
    if (true) {
      this.subsHead = void 0;
    }
  }
  track(debugInfo) {
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return;
    }
    let link = this.activeLink;
    if (link === void 0 || link.sub !== activeSub) {
      link = this.activeLink = new Link(activeSub, this);
      if (!activeSub.deps) {
        activeSub.deps = activeSub.depsTail = link;
      } else {
        link.prevDep = activeSub.depsTail;
        activeSub.depsTail.nextDep = link;
        activeSub.depsTail = link;
      }
      addSub(link);
    } else if (link.version === -1) {
      link.version = this.version;
      if (link.nextDep) {
        const next = link.nextDep;
        next.prevDep = link.prevDep;
        if (link.prevDep) {
          link.prevDep.nextDep = next;
        }
        link.prevDep = activeSub.depsTail;
        link.nextDep = void 0;
        activeSub.depsTail.nextDep = link;
        activeSub.depsTail = link;
        if (activeSub.deps === link) {
          activeSub.deps = next;
        }
      }
    }
    if (activeSub.onTrack) {
      activeSub.onTrack(
        extend(
          {
            effect: activeSub
          },
          debugInfo
        )
      );
    }
    return link;
  }
  trigger(debugInfo) {
    this.version++;
    globalVersion++;
    this.notify(debugInfo);
  }
  notify(debugInfo) {
    startBatch();
    try {
      if (true) {
        for (let head = this.subsHead; head; head = head.nextSub) {
          if (head.sub.onTrigger && !(head.sub.flags & 8)) {
            head.sub.onTrigger(
              extend(
                {
                  effect: head.sub
                },
                debugInfo
              )
            );
          }
        }
      }
      for (let link = this.subs; link; link = link.prevSub) {
        if (link.sub.notify()) {
          ;
          link.sub.dep.notify();
        }
      }
    } finally {
      endBatch();
    }
  }
};
function addSub(link) {
  link.dep.sc++;
  if (link.sub.flags & 4) {
    const computed3 = link.dep.computed;
    if (computed3 && !link.dep.subs) {
      computed3.flags |= 4 | 16;
      for (let l = computed3.deps; l; l = l.nextDep) {
        addSub(l);
      }
    }
    const currentTail = link.dep.subs;
    if (currentTail !== link) {
      link.prevSub = currentTail;
      if (currentTail) currentTail.nextSub = link;
    }
    if (link.dep.subsHead === void 0) {
      link.dep.subsHead = link;
    }
    link.dep.subs = link;
  }
}
var targetMap = /* @__PURE__ */ new WeakMap();
var ITERATE_KEY = Symbol(
  true ? "Object iterate" : ""
);
var MAP_KEY_ITERATE_KEY = Symbol(
  true ? "Map keys iterate" : ""
);
var ARRAY_ITERATE_KEY = Symbol(
  true ? "Array iterate" : ""
);
function track(target, type, key) {
  if (shouldTrack && activeSub) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, depsMap = /* @__PURE__ */ new Map());
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, dep = new Dep());
      dep.map = depsMap;
      dep.key = key;
    }
    if (true) {
      dep.track({
        target,
        type,
        key
      });
    } else {
      dep.track();
    }
  }
}
function trigger(target, type, key, newValue, oldValue, oldTarget) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    globalVersion++;
    return;
  }
  const run = (dep) => {
    if (dep) {
      if (true) {
        dep.trigger({
          target,
          type,
          key,
          newValue,
          oldValue,
          oldTarget
        });
      } else {
        dep.trigger();
      }
    }
  };
  startBatch();
  if (type === "clear") {
    depsMap.forEach(run);
  } else {
    const targetIsArray = isArray(target);
    const isArrayIndex = targetIsArray && isIntegerKey(key);
    if (targetIsArray && key === "length") {
      const newLength = Number(newValue);
      depsMap.forEach((dep, key2) => {
        if (key2 === "length" || key2 === ARRAY_ITERATE_KEY || !isSymbol(key2) && key2 >= newLength) {
          run(dep);
        }
      });
    } else {
      if (key !== void 0 || depsMap.has(void 0)) {
        run(depsMap.get(key));
      }
      if (isArrayIndex) {
        run(depsMap.get(ARRAY_ITERATE_KEY));
      }
      switch (type) {
        case "add":
          if (!targetIsArray) {
            run(depsMap.get(ITERATE_KEY));
            if (isMap(target)) {
              run(depsMap.get(MAP_KEY_ITERATE_KEY));
            }
          } else if (isArrayIndex) {
            run(depsMap.get("length"));
          }
          break;
        case "delete":
          if (!targetIsArray) {
            run(depsMap.get(ITERATE_KEY));
            if (isMap(target)) {
              run(depsMap.get(MAP_KEY_ITERATE_KEY));
            }
          }
          break;
        case "set":
          if (isMap(target)) {
            run(depsMap.get(ITERATE_KEY));
          }
          break;
      }
    }
  }
  endBatch();
}
function getDepFromReactive(object, key) {
  const depMap = targetMap.get(object);
  return depMap && depMap.get(key);
}
function reactiveReadArray(array) {
  const raw = toRaw(array);
  if (raw === array) return raw;
  track(raw, "iterate", ARRAY_ITERATE_KEY);
  return isShallow(array) ? raw : raw.map(toReactive);
}
function shallowReadArray(arr) {
  track(arr = toRaw(arr), "iterate", ARRAY_ITERATE_KEY);
  return arr;
}
var arrayInstrumentations = {
  __proto__: null,
  [Symbol.iterator]() {
    return iterator(this, Symbol.iterator, toReactive);
  },
  concat(...args) {
    return reactiveReadArray(this).concat(
      ...args.map((x) => isArray(x) ? reactiveReadArray(x) : x)
    );
  },
  entries() {
    return iterator(this, "entries", (value) => {
      value[1] = toReactive(value[1]);
      return value;
    });
  },
  every(fn, thisArg) {
    return apply(this, "every", fn, thisArg, void 0, arguments);
  },
  filter(fn, thisArg) {
    return apply(this, "filter", fn, thisArg, (v) => v.map(toReactive), arguments);
  },
  find(fn, thisArg) {
    return apply(this, "find", fn, thisArg, toReactive, arguments);
  },
  findIndex(fn, thisArg) {
    return apply(this, "findIndex", fn, thisArg, void 0, arguments);
  },
  findLast(fn, thisArg) {
    return apply(this, "findLast", fn, thisArg, toReactive, arguments);
  },
  findLastIndex(fn, thisArg) {
    return apply(this, "findLastIndex", fn, thisArg, void 0, arguments);
  },
  // flat, flatMap could benefit from ARRAY_ITERATE but are not straight-forward to implement
  forEach(fn, thisArg) {
    return apply(this, "forEach", fn, thisArg, void 0, arguments);
  },
  includes(...args) {
    return searchProxy(this, "includes", args);
  },
  indexOf(...args) {
    return searchProxy(this, "indexOf", args);
  },
  join(separator) {
    return reactiveReadArray(this).join(separator);
  },
  // keys() iterator only reads `length`, no optimisation required
  lastIndexOf(...args) {
    return searchProxy(this, "lastIndexOf", args);
  },
  map(fn, thisArg) {
    return apply(this, "map", fn, thisArg, void 0, arguments);
  },
  pop() {
    return noTracking(this, "pop");
  },
  push(...args) {
    return noTracking(this, "push", args);
  },
  reduce(fn, ...args) {
    return reduce(this, "reduce", fn, args);
  },
  reduceRight(fn, ...args) {
    return reduce(this, "reduceRight", fn, args);
  },
  shift() {
    return noTracking(this, "shift");
  },
  // slice could use ARRAY_ITERATE but also seems to beg for range tracking
  some(fn, thisArg) {
    return apply(this, "some", fn, thisArg, void 0, arguments);
  },
  splice(...args) {
    return noTracking(this, "splice", args);
  },
  toReversed() {
    return reactiveReadArray(this).toReversed();
  },
  toSorted(comparer) {
    return reactiveReadArray(this).toSorted(comparer);
  },
  toSpliced(...args) {
    return reactiveReadArray(this).toSpliced(...args);
  },
  unshift(...args) {
    return noTracking(this, "unshift", args);
  },
  values() {
    return iterator(this, "values", toReactive);
  }
};
function iterator(self2, method, wrapValue) {
  const arr = shallowReadArray(self2);
  const iter = arr[method]();
  if (arr !== self2 && !isShallow(self2)) {
    iter._next = iter.next;
    iter.next = () => {
      const result = iter._next();
      if (result.value) {
        result.value = wrapValue(result.value);
      }
      return result;
    };
  }
  return iter;
}
var arrayProto = Array.prototype;
function apply(self2, method, fn, thisArg, wrappedRetFn, args) {
  const arr = shallowReadArray(self2);
  const needsWrap = arr !== self2 && !isShallow(self2);
  const methodFn = arr[method];
  if (methodFn !== arrayProto[method]) {
    const result2 = methodFn.apply(self2, args);
    return needsWrap ? toReactive(result2) : result2;
  }
  let wrappedFn = fn;
  if (arr !== self2) {
    if (needsWrap) {
      wrappedFn = function(item, index) {
        return fn.call(this, toReactive(item), index, self2);
      };
    } else if (fn.length > 2) {
      wrappedFn = function(item, index) {
        return fn.call(this, item, index, self2);
      };
    }
  }
  const result = methodFn.call(arr, wrappedFn, thisArg);
  return needsWrap && wrappedRetFn ? wrappedRetFn(result) : result;
}
function reduce(self2, method, fn, args) {
  const arr = shallowReadArray(self2);
  let wrappedFn = fn;
  if (arr !== self2) {
    if (!isShallow(self2)) {
      wrappedFn = function(acc, item, index) {
        return fn.call(this, acc, toReactive(item), index, self2);
      };
    } else if (fn.length > 3) {
      wrappedFn = function(acc, item, index) {
        return fn.call(this, acc, item, index, self2);
      };
    }
  }
  return arr[method](wrappedFn, ...args);
}
function searchProxy(self2, method, args) {
  const arr = toRaw(self2);
  track(arr, "iterate", ARRAY_ITERATE_KEY);
  const res = arr[method](...args);
  if ((res === -1 || res === false) && isProxy(args[0])) {
    args[0] = toRaw(args[0]);
    return arr[method](...args);
  }
  return res;
}
function noTracking(self2, method, args = []) {
  pauseTracking();
  startBatch();
  const res = toRaw(self2)[method].apply(self2, args);
  endBatch();
  resetTracking();
  return res;
}
var isNonTrackableKeys = makeMap(`__proto__,__v_isRef,__isVue`);
var builtInSymbols = new Set(
  Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(isSymbol)
);
function hasOwnProperty2(key) {
  if (!isSymbol(key)) key = String(key);
  const obj = toRaw(this);
  track(obj, "has", key);
  return obj.hasOwnProperty(key);
}
var BaseReactiveHandler = class {
  constructor(_isReadonly = false, _isShallow = false) {
    this._isReadonly = _isReadonly;
    this._isShallow = _isShallow;
  }
  get(target, key, receiver) {
    if (key === "__v_skip") return target["__v_skip"];
    const isReadonly2 = this._isReadonly, isShallow2 = this._isShallow;
    if (key === "__v_isReactive") {
      return !isReadonly2;
    } else if (key === "__v_isReadonly") {
      return isReadonly2;
    } else if (key === "__v_isShallow") {
      return isShallow2;
    } else if (key === "__v_raw") {
      if (receiver === (isReadonly2 ? isShallow2 ? shallowReadonlyMap : readonlyMap : isShallow2 ? shallowReactiveMap : reactiveMap).get(target) || // receiver is not the reactive proxy, but has the same prototype
      // this means the receiver is a user proxy of the reactive proxy
      Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)) {
        return target;
      }
      return;
    }
    const targetIsArray = isArray(target);
    if (!isReadonly2) {
      let fn;
      if (targetIsArray && (fn = arrayInstrumentations[key])) {
        return fn;
      }
      if (key === "hasOwnProperty") {
        return hasOwnProperty2;
      }
    }
    const res = Reflect.get(
      target,
      key,
      // if this is a proxy wrapping a ref, return methods using the raw ref
      // as receiver so that we don't have to call `toRaw` on the ref in all
      // its class methods
      isRef2(target) ? target : receiver
    );
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res;
    }
    if (!isReadonly2) {
      track(target, "get", key);
    }
    if (isShallow2) {
      return res;
    }
    if (isRef2(res)) {
      return targetIsArray && isIntegerKey(key) ? res : res.value;
    }
    if (isObject(res)) {
      return isReadonly2 ? readonly(res) : reactive(res);
    }
    return res;
  }
};
var MutableReactiveHandler = class extends BaseReactiveHandler {
  constructor(isShallow2 = false) {
    super(false, isShallow2);
  }
  set(target, key, value, receiver) {
    let oldValue = target[key];
    if (!this._isShallow) {
      const isOldValueReadonly = isReadonly(oldValue);
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue);
        value = toRaw(value);
      }
      if (!isArray(target) && isRef2(oldValue) && !isRef2(value)) {
        if (isOldValueReadonly) {
          return false;
        } else {
          oldValue.value = value;
          return true;
        }
      }
    }
    const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);
    const result = Reflect.set(
      target,
      key,
      value,
      isRef2(target) ? target : receiver
    );
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, "add", key, value);
      } else if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
  deleteProperty(target, key) {
    const hadKey = hasOwn(target, key);
    const oldValue = target[key];
    const result = Reflect.deleteProperty(target, key);
    if (result && hadKey) {
      trigger(target, "delete", key, void 0, oldValue);
    }
    return result;
  }
  has(target, key) {
    const result = Reflect.has(target, key);
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
      track(target, "has", key);
    }
    return result;
  }
  ownKeys(target) {
    track(
      target,
      "iterate",
      isArray(target) ? "length" : ITERATE_KEY
    );
    return Reflect.ownKeys(target);
  }
};
var ReadonlyReactiveHandler = class extends BaseReactiveHandler {
  constructor(isShallow2 = false) {
    super(true, isShallow2);
  }
  set(target, key) {
    if (true) {
      warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true;
  }
  deleteProperty(target, key) {
    if (true) {
      warn(
        `Delete operation on key "${String(key)}" failed: target is readonly.`,
        target
      );
    }
    return true;
  }
};
var mutableHandlers = new MutableReactiveHandler();
var readonlyHandlers = new ReadonlyReactiveHandler();
var shallowReactiveHandlers = new MutableReactiveHandler(true);
var shallowReadonlyHandlers = new ReadonlyReactiveHandler(true);
var toShallow = (value) => value;
var getProto = (v) => Reflect.getPrototypeOf(v);
function createIterableMethod(method, isReadonly2, isShallow2) {
  return function(...args) {
    const target = this["__v_raw"];
    const rawTarget = toRaw(target);
    const targetIsMap = isMap(rawTarget);
    const isPair = method === "entries" || method === Symbol.iterator && targetIsMap;
    const isKeyOnly = method === "keys" && targetIsMap;
    const innerIterator = target[method](...args);
    const wrap = isShallow2 ? toShallow : isReadonly2 ? toReadonly : toReactive;
    !isReadonly2 && track(
      rawTarget,
      "iterate",
      isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
    );
    return {
      // iterator protocol
      next() {
        const { value, done } = innerIterator.next();
        return done ? { value, done } : {
          value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
          done
        };
      },
      // iterable protocol
      [Symbol.iterator]() {
        return this;
      }
    };
  };
}
function createReadonlyMethod(type) {
  return function(...args) {
    if (true) {
      const key = args[0] ? `on key "${args[0]}" ` : ``;
      warn(
        `${capitalize(type)} operation ${key}failed: target is readonly.`,
        toRaw(this)
      );
    }
    return type === "delete" ? false : type === "clear" ? void 0 : this;
  };
}
function createInstrumentations(readonly2, shallow) {
  const instrumentations = {
    get(key) {
      const target = this["__v_raw"];
      const rawTarget = toRaw(target);
      const rawKey = toRaw(key);
      if (!readonly2) {
        if (hasChanged(key, rawKey)) {
          track(rawTarget, "get", key);
        }
        track(rawTarget, "get", rawKey);
      }
      const { has } = getProto(rawTarget);
      const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive;
      if (has.call(rawTarget, key)) {
        return wrap(target.get(key));
      } else if (has.call(rawTarget, rawKey)) {
        return wrap(target.get(rawKey));
      } else if (target !== rawTarget) {
        target.get(key);
      }
    },
    get size() {
      const target = this["__v_raw"];
      !readonly2 && track(toRaw(target), "iterate", ITERATE_KEY);
      return Reflect.get(target, "size", target);
    },
    has(key) {
      const target = this["__v_raw"];
      const rawTarget = toRaw(target);
      const rawKey = toRaw(key);
      if (!readonly2) {
        if (hasChanged(key, rawKey)) {
          track(rawTarget, "has", key);
        }
        track(rawTarget, "has", rawKey);
      }
      return key === rawKey ? target.has(key) : target.has(key) || target.has(rawKey);
    },
    forEach(callback, thisArg) {
      const observed = this;
      const target = observed["__v_raw"];
      const rawTarget = toRaw(target);
      const wrap = shallow ? toShallow : readonly2 ? toReadonly : toReactive;
      !readonly2 && track(rawTarget, "iterate", ITERATE_KEY);
      return target.forEach((value, key) => {
        return callback.call(thisArg, wrap(value), wrap(key), observed);
      });
    }
  };
  extend(
    instrumentations,
    readonly2 ? {
      add: createReadonlyMethod("add"),
      set: createReadonlyMethod("set"),
      delete: createReadonlyMethod("delete"),
      clear: createReadonlyMethod("clear")
    } : {
      add(value) {
        if (!shallow && !isShallow(value) && !isReadonly(value)) {
          value = toRaw(value);
        }
        const target = toRaw(this);
        const proto = getProto(target);
        const hadKey = proto.has.call(target, value);
        if (!hadKey) {
          target.add(value);
          trigger(target, "add", value, value);
        }
        return this;
      },
      set(key, value) {
        if (!shallow && !isShallow(value) && !isReadonly(value)) {
          value = toRaw(value);
        }
        const target = toRaw(this);
        const { has, get } = getProto(target);
        let hadKey = has.call(target, key);
        if (!hadKey) {
          key = toRaw(key);
          hadKey = has.call(target, key);
        } else if (true) {
          checkIdentityKeys(target, has, key);
        }
        const oldValue = get.call(target, key);
        target.set(key, value);
        if (!hadKey) {
          trigger(target, "add", key, value);
        } else if (hasChanged(value, oldValue)) {
          trigger(target, "set", key, value, oldValue);
        }
        return this;
      },
      delete(key) {
        const target = toRaw(this);
        const { has, get } = getProto(target);
        let hadKey = has.call(target, key);
        if (!hadKey) {
          key = toRaw(key);
          hadKey = has.call(target, key);
        } else if (true) {
          checkIdentityKeys(target, has, key);
        }
        const oldValue = get ? get.call(target, key) : void 0;
        const result = target.delete(key);
        if (hadKey) {
          trigger(target, "delete", key, void 0, oldValue);
        }
        return result;
      },
      clear() {
        const target = toRaw(this);
        const hadItems = target.size !== 0;
        const oldTarget = true ? isMap(target) ? new Map(target) : new Set(target) : void 0;
        const result = target.clear();
        if (hadItems) {
          trigger(
            target,
            "clear",
            void 0,
            void 0,
            oldTarget
          );
        }
        return result;
      }
    }
  );
  const iteratorMethods = [
    "keys",
    "values",
    "entries",
    Symbol.iterator
  ];
  iteratorMethods.forEach((method) => {
    instrumentations[method] = createIterableMethod(method, readonly2, shallow);
  });
  return instrumentations;
}
function createInstrumentationGetter(isReadonly2, shallow) {
  const instrumentations = createInstrumentations(isReadonly2, shallow);
  return (target, key, receiver) => {
    if (key === "__v_isReactive") {
      return !isReadonly2;
    } else if (key === "__v_isReadonly") {
      return isReadonly2;
    } else if (key === "__v_raw") {
      return target;
    }
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target ? instrumentations : target,
      key,
      receiver
    );
  };
}
var mutableCollectionHandlers = {
  get: createInstrumentationGetter(false, false)
};
var shallowCollectionHandlers = {
  get: createInstrumentationGetter(false, true)
};
var readonlyCollectionHandlers = {
  get: createInstrumentationGetter(true, false)
};
var shallowReadonlyCollectionHandlers = {
  get: createInstrumentationGetter(true, true)
};
function checkIdentityKeys(target, has, key) {
  const rawKey = toRaw(key);
  if (rawKey !== key && has.call(target, rawKey)) {
    const type = toRawType(target);
    warn(
      `Reactive ${type} contains both the raw and reactive versions of the same object${type === `Map` ? ` as keys` : ``}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`
    );
  }
}
var reactiveMap = /* @__PURE__ */ new WeakMap();
var shallowReactiveMap = /* @__PURE__ */ new WeakMap();
var readonlyMap = /* @__PURE__ */ new WeakMap();
var shallowReadonlyMap = /* @__PURE__ */ new WeakMap();
function targetTypeMap(rawType) {
  switch (rawType) {
    case "Object":
    case "Array":
      return 1;
    case "Map":
    case "Set":
    case "WeakMap":
    case "WeakSet":
      return 2;
    default:
      return 0;
  }
}
function getTargetType(value) {
  return value["__v_skip"] || !Object.isExtensible(value) ? 0 : targetTypeMap(toRawType(value));
}
function reactive(target) {
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}
function shallowReactive(target) {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers,
    shallowReactiveMap
  );
}
function readonly(target) {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers,
    readonlyMap
  );
}
function shallowReadonly(target) {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    shallowReadonlyCollectionHandlers,
    shallowReadonlyMap
  );
}
function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) {
  if (!isObject(target)) {
    if (true) {
      warn(
        `value cannot be made ${isReadonly2 ? "readonly" : "reactive"}: ${String(
          target
        )}`
      );
    }
    return target;
  }
  if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) {
    return target;
  }
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  const targetType = getTargetType(target);
  if (targetType === 0) {
    return target;
  }
  const proxy = new Proxy(
    target,
    targetType === 2 ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}
function isReactive(value) {
  if (isReadonly(value)) {
    return isReactive(value["__v_raw"]);
  }
  return !!(value && value["__v_isReactive"]);
}
function isReadonly(value) {
  return !!(value && value["__v_isReadonly"]);
}
function isShallow(value) {
  return !!(value && value["__v_isShallow"]);
}
function isProxy(value) {
  return value ? !!value["__v_raw"] : false;
}
function toRaw(observed) {
  const raw = observed && observed["__v_raw"];
  return raw ? toRaw(raw) : observed;
}
function markRaw(value) {
  if (!hasOwn(value, "__v_skip") && Object.isExtensible(value)) {
    def(value, "__v_skip", true);
  }
  return value;
}
var toReactive = (value) => isObject(value) ? reactive(value) : value;
var toReadonly = (value) => isObject(value) ? readonly(value) : value;
function isRef2(r) {
  return r ? r["__v_isRef"] === true : false;
}
function ref(value) {
  return createRef(value, false);
}
function shallowRef(value) {
  return createRef(value, true);
}
function createRef(rawValue, shallow) {
  if (isRef2(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}
var RefImpl = class {
  constructor(value, isShallow2) {
    this.dep = new Dep();
    this["__v_isRef"] = true;
    this["__v_isShallow"] = false;
    this._rawValue = isShallow2 ? value : toRaw(value);
    this._value = isShallow2 ? value : toReactive(value);
    this["__v_isShallow"] = isShallow2;
  }
  get value() {
    if (true) {
      this.dep.track({
        target: this,
        type: "get",
        key: "value"
      });
    } else {
      this.dep.track();
    }
    return this._value;
  }
  set value(newValue) {
    const oldValue = this._rawValue;
    const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue);
    newValue = useDirectValue ? newValue : toRaw(newValue);
    if (hasChanged(newValue, oldValue)) {
      this._rawValue = newValue;
      this._value = useDirectValue ? newValue : toReactive(newValue);
      if (true) {
        this.dep.trigger({
          target: this,
          type: "set",
          key: "value",
          newValue,
          oldValue
        });
      } else {
        this.dep.trigger();
      }
    }
  }
};
function triggerRef(ref2) {
  if (ref2.dep) {
    if (true) {
      ref2.dep.trigger({
        target: ref2,
        type: "set",
        key: "value",
        newValue: ref2._value
      });
    } el
Download .txt
gitextract_50fi8_rw/

├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── custom.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── docker-image.yml
│       └── docs.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── SECURITY.md
├── apps/
│   ├── __init__.py
│   ├── admin/
│   │   ├── __init__.py
│   │   ├── dependencies.py
│   │   ├── schemas.py
│   │   ├── services.py
│   │   └── views.py
│   └── base/
│       ├── __init__.py
│       ├── dependencies.py
│       ├── migrations/
│       │   ├── migrations_001.py
│       │   ├── migrations_002.py
│       │   ├── migrations_003.py
│       │   ├── migrations_004.py
│       │   └── migrations_005.py
│       ├── models.py
│       ├── schemas.py
│       ├── utils.py
│       └── views.py
├── core/
│   ├── __init__.py
│   ├── config.py
│   ├── database.py
│   ├── logger.py
│   ├── response.py
│   ├── settings.py
│   ├── storage.py
│   ├── tasks.py
│   └── utils.py
├── docker-compose.yml
├── docs/
│   ├── .vitepress/
│   │   ├── cache/
│   │   │   └── deps/
│   │   │       ├── _metadata.json
│   │   │       ├── chunk-CQOUZRMK.js
│   │   │       ├── chunk-KT7LHMJ2.js
│   │   │       ├── package.json
│   │   │       ├── vitepress___@vue_devtools-api.js
│   │   │       ├── vitepress___@vueuse_core.js
│   │   │       ├── vitepress___@vueuse_integrations_useFocusTrap.js
│   │   │       ├── vitepress___mark__js_src_vanilla__js.js
│   │   │       ├── vitepress___minisearch.js
│   │   │       └── vue.js
│   │   ├── config.mts
│   │   └── theme/
│   │       ├── custom.css
│   │       └── index.ts
│   ├── api/
│   │   ├── index.md
│   │   └── presign-upload.md
│   ├── changelog.md
│   ├── contributing.md
│   ├── en/
│   │   ├── api/
│   │   │   └── index.md
│   │   ├── changelog.md
│   │   ├── contributing.md
│   │   ├── guide/
│   │   │   ├── configuration.md
│   │   │   ├── getting-started.md
│   │   │   ├── introduction.md
│   │   │   ├── management.md
│   │   │   ├── security.md
│   │   │   ├── share.md
│   │   │   ├── storage.md
│   │   │   └── upload.md
│   │   ├── index.md
│   │   └── showcase.md
│   ├── guide/
│   │   ├── configuration.md
│   │   ├── getting-started.md
│   │   ├── introduction.md
│   │   ├── management.md
│   │   ├── security.md
│   │   ├── share.md
│   │   ├── storage-onedrive.md
│   │   ├── storage-opendal.md
│   │   ├── storage.md
│   │   └── upload.md
│   ├── index.md
│   ├── package.json
│   └── showcase.md
├── main.py
├── readme.md
├── readme_en.md
└── requirements.txt
Download .txt
SYMBOL INDEX (1327 symbols across 29 files)

FILE: apps/admin/dependencies.py
  function create_token (line 15) | def create_token(data: dict, expires_in: int = 3600 * 24 * 30) -> str:
  function verify_token (line 36) | def verify_token(token: str) -> dict:
  function _extract_bearer_token (line 68) | def _extract_bearer_token(authorization: str) -> str:
  function _require_admin_payload (line 77) | def _require_admin_payload(authorization: str) -> dict:
  function admin_required (line 91) | async def admin_required(
  function share_required_login (line 102) | async def share_required_login(authorization: str = Header(default=None)):
  function get_file_service (line 123) | async def get_file_service():
  function get_config_service (line 127) | async def get_config_service():
  function get_local_file_service (line 131) | async def get_local_file_service():

FILE: apps/admin/schemas.py
  class IDData (line 7) | class IDData(BaseModel):
  class ShareItem (line 11) | class ShareItem(BaseModel):
  class DeleteItem (line 17) | class DeleteItem(BaseModel):
  class LoginData (line 21) | class LoginData(BaseModel):
  class UpdateFileData (line 25) | class UpdateFileData(BaseModel):

FILE: apps/admin/services.py
  class FileService (line 14) | class FileService:
    method __init__ (line 15) | def __init__(self):
    method delete_file (line 18) | async def delete_file(self, file_id: int):
    method list_files (line 23) | async def list_files(self, page: int, size: int, keyword: str = ""):
    method download_file (line 34) | async def download_file(self, file_id: int):
    method share_local_file (line 43) | async def share_local_file(self, item):
  class ConfigService (line 74) | class ConfigService:
    method get_config (line 75) | def get_config(self):
    method update_config (line 78) | async def update_config(self, data: dict):
  class LocalFileService (line 112) | class LocalFileService:
    method list_files (line 113) | async def list_files(self):
    method delete_file (line 126) | async def delete_file(self, filename: str):
  class LocalFileClass (line 134) | class LocalFileClass:
    method __init__ (line 135) | def __init__(self, file):
    method read (line 147) | async def read(self):
    method write (line 150) | async def write(self, data):
    method delete (line 154) | async def delete(self):
    method exists (line 157) | async def exists(self):

FILE: apps/admin/views.py
  function login (line 28) | async def login(data: LoginData):
  function dashboard (line 37) | async def dashboard():
  function file_delete (line 63) | async def file_delete(
  function file_list (line 72) | async def file_list(
  function get_config (line 90) | async def get_config(
  function update_config (line 97) | async def update_config(
  function file_download (line 107) | async def file_download(
  function get_local_lists (line 116) | async def get_local_lists(
  function delete_local_file (line 124) | async def delete_local_file(
  function share_local_file (line 133) | async def share_local_file(
  function update_file (line 142) | async def update_file(

FILE: apps/base/dependencies.py
  class IPRateLimit (line 6) | class IPRateLimit:
    method __init__ (line 7) | def __init__(self, count: int, minutes: int):
    method check_ip (line 12) | def check_ip(self, ip: str) -> bool:
    method add_ip (line 21) | def add_ip(self, ip: str) -> int:
    method remove_expired_ip (line 28) | async def remove_expired_ip(self) -> None:
    method __call__ (line 37) | def __call__(self, request: Request) -> str:

FILE: apps/base/migrations/migrations_001.py
  function create_file_codes_table (line 4) | async def create_file_codes_table():
  function create_key_value_table (line 31) | async def create_key_value_table():
  function migrate (line 50) | async def migrate():

FILE: apps/base/migrations/migrations_002.py
  function create_upload_chunk_and_update_file_codes_table (line 4) | async def create_upload_chunk_and_update_file_codes_table():
  function migrate (line 27) | async def migrate():

FILE: apps/base/migrations/migrations_003.py
  function create_presign_upload_session_table (line 4) | async def create_presign_upload_session_table():
  function migrate (line 25) | async def migrate():

FILE: apps/base/migrations/migrations_004.py
  function add_save_path_to_uploadchunk (line 4) | async def add_save_path_to_uploadchunk():
  function migrate (line 13) | async def migrate():

FILE: apps/base/migrations/migrations_005.py
  function _need_upgrade (line 4) | def _need_upgrade(columns: list[tuple]) -> bool:
  function migrate (line 13) | async def migrate():

FILE: apps/base/models.py
  class FileCodes (line 15) | class FileCodes(models.Model):
    method is_expired (line 32) | async def is_expired(self):
    method get_file_path (line 39) | async def get_file_path(self):
  class UploadChunk (line 43) | class UploadChunk(models.Model):
  class KeyValue (line 57) | class KeyValue(Model):
  class PresignUploadSession (line 68) | class PresignUploadSession(models.Model):
    method is_expired (line 82) | async def is_expired(self):

FILE: apps/base/schemas.py
  class SelectFileModel (line 5) | class SelectFileModel(BaseModel):
  class InitChunkUploadModel (line 9) | class InitChunkUploadModel(BaseModel):
  class CompleteUploadModel (line 16) | class CompleteUploadModel(BaseModel):
  class PresignUploadInitRequest (line 22) | class PresignUploadInitRequest(BaseModel):
  class PresignUploadInitResponse (line 30) | class PresignUploadInitResponse(BaseModel):

FILE: apps/base/utils.py
  function get_file_path_name (line 22) | async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, s...
  function get_chunk_file_path_name (line 34) | async def get_chunk_file_path_name(
  function get_expire_info (line 46) | async def get_expire_info(
  function get_random_code (line 94) | async def get_random_code(style: str = "num") -> str:
  function calculate_file_hash (line 101) | async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) ...

FILE: apps/base/views.py
  class FileUploadService (line 36) | class FileUploadService:
    method generate_file_path (line 40) | async def generate_file_path(
    method create_file_record (line 55) | async def create_file_record(
  function validate_file_size (line 84) | async def validate_file_size(file: UploadFile, max_size: int) -> int:
  function create_file_code (line 98) | async def create_file_code(code, **kwargs):
  function share_text (line 103) | async def share_text(
  function share_file (line 131) | async def share_file(
  function get_code_file_by_code (line 161) | async def get_code_file_by_code(
  function update_file_usage (line 172) | async def update_file_usage(file_code: FileCodes) -> None:
  function get_code_file (line 180) | async def get_code_file(code: str, ip: str = Depends(ip_limit["error"])):
  function select_file (line 193) | async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit[...
  function download_file (line 217) | async def download_file(key: str, code: str, ip: str = Depends(ip_limit[...
  function init_chunk_upload (line 237) | async def init_chunk_upload(data: InitChunkUploadModel):
  function upload_chunk (line 315) | async def upload_chunk(
  function cancel_upload (line 394) | async def cancel_upload(upload_id: str):
  function get_upload_status (line 419) | async def get_upload_status(upload_id: str):
  function complete_upload (line 446) | async def complete_upload(
  function _get_valid_session (line 527) | async def _get_valid_session(
  function presign_upload_init (line 543) | async def presign_upload_init(
  function presign_upload_proxy (line 593) | async def presign_upload_proxy(
  function presign_upload_confirm (line 625) | async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_li...
  function presign_upload_status (line 649) | async def presign_upload_status(upload_id: str):
  function presign_upload_cancel (line 669) | async def presign_upload_cancel(upload_id: str):

FILE: core/config.py
  function ensure_settings_row (line 6) | async def ensure_settings_row() -> None:
  function _sync_ip_limits (line 10) | def _sync_ip_limits() -> None:
  function refresh_settings (line 17) | async def refresh_settings() -> None:

FILE: core/database.py
  function get_db_config (line 18) | def get_db_config() -> dict:
  function _lock_file (line 42) | def _lock_file(file_obj: IO[str]) -> None:
  function _unlock_file (line 57) | def _unlock_file(file_obj: IO[str]) -> None:
  function db_startup_lock (line 69) | async def db_startup_lock():
  function init_db (line 80) | async def init_db():
  function execute_migrations (line 105) | async def execute_migrations():

FILE: core/logger.py
  function setup_logger (line 5) | def setup_logger():

FILE: core/response.py
  class APIResponse (line 12) | class APIResponse(BaseModel, Generic[T]):

FILE: core/settings.py
  class Settings (line 77) | class Settings:
    method __init__ (line 78) | def __init__(self, defaults=None):
    method __getattr__ (line 82) | def __getattr__(self, attr):
    method __setattr__ (line 91) | def __setattr__(self, key, value):
    method items (line 97) | def items(self):

FILE: core/storage.py
  class FileStorageInterface (line 30) | class FileStorageInterface:
    method save_file (line 32) | async def save_file(self, file: UploadFile, save_path: str):
    method delete_file (line 38) | async def delete_file(self, file_code: FileCodes):
    method get_file_url (line 44) | async def get_file_url(self, file_code: FileCodes):
    method get_file_response (line 53) | async def get_file_response(self, file_code: FileCodes):
    method save_chunk (line 62) | async def save_chunk(self, upload_id: str, chunk_index: int, chunk_dat...
    method merge_chunks (line 73) | async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, ...
    method generate_presigned_upload_url (line 83) | async def generate_presigned_upload_url(self, save_path: str, expires_...
    method file_exists (line 92) | async def file_exists(self, save_path: str) -> bool:
    method clean_chunks (line 100) | async def clean_chunks(self, upload_id: str, save_path: str):
  class SystemFileStorage (line 109) | class SystemFileStorage(FileStorageInterface):
    method __init__ (line 110) | def __init__(self):
    method _save (line 114) | def _save(self, file, save_path):
    method save_file (line 121) | async def save_file(self, file: UploadFile, save_path: str):
    method delete_file (line 133) | async def delete_file(self, file_code: FileCodes):
    method get_file_url (line 138) | async def get_file_url(self, file_code: FileCodes):
    method get_file_response (line 141) | async def get_file_response(self, file_code: FileCodes):
    method save_chunk (line 165) | async def save_chunk(self, upload_id: str, chunk_index: int, chunk_dat...
    method merge_chunks (line 190) | async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, ...
    method clean_chunks (line 230) | async def clean_chunks(self, upload_id: str, save_path: str):
    method file_exists (line 250) | async def file_exists(self, save_path: str) -> bool:
  class S3FileStorage (line 260) | class S3FileStorage(FileStorageInterface):
    method __init__ (line 261) | def __init__(self):
    method save_file (line 281) | async def save_file(self, file: UploadFile, save_path: str):
    method delete_file (line 297) | async def delete_file(self, file_code: FileCodes):
    method get_file_response (line 308) | async def get_file_response(self, file_code: FileCodes):
    method get_file_url (line 381) | async def get_file_url(self, file_code: FileCodes):
    method save_chunk (line 403) | async def save_chunk(self, upload_id: str, chunk_index: int, chunk_dat...
    method merge_chunks (line 427) | async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, ...
    method clean_chunks (line 508) | async def clean_chunks(self, upload_id: str, save_path: str):
    method generate_presigned_upload_url (line 536) | async def generate_presigned_upload_url(self, save_path: str, expires_...
    method file_exists (line 559) | async def file_exists(self, save_path: str) -> bool:
  class OneDriveFileStorage (line 579) | class OneDriveFileStorage(FileStorageInterface):
    method __init__ (line 580) | def __init__(self):
    method acquire_token_pwd (line 618) | def acquire_token_pwd(self):
    method _get_path_str (line 630) | def _get_path_str(self, path):
    method _save (line 640) | def _save(self, file, save_path):
    method save_file (line 646) | async def save_file(self, file: UploadFile, save_path: str):
    method _delete (line 649) | def _delete(self, save_path):
    method delete_file (line 659) | async def delete_file(self, file_code: FileCodes):
    method _convert_link_to_download_link (line 662) | def _convert_link_to_download_link(self, link):
    method _get_file_url (line 668) | def _get_file_url(self, save_path, name):
    method get_file_response (line 681) | async def get_file_response(self, file_code: FileCodes):
    method get_file_url (line 735) | async def get_file_url(self, file_code: FileCodes):
    method _save_chunk (line 745) | def _save_chunk(self, chunk_path: str, chunk_data: bytes):
    method save_chunk (line 766) | async def save_chunk(self, upload_id: str, chunk_index: int, chunk_dat...
    method _read_chunk (line 771) | def _read_chunk(self, chunk_path: str) -> bytes:
    method _upload_merged (line 777) | def _upload_merged(self, save_path: str, data: bytes):
    method merge_chunks (line 797) | async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, ...
    method _delete_chunk_dir (line 838) | def _delete_chunk_dir(self, chunk_dir: str):
    method clean_chunks (line 847) | async def clean_chunks(self, upload_id: str, save_path: str):
    method _file_exists (line 855) | def _file_exists(self, save_path: str) -> bool:
    method file_exists (line 866) | async def file_exists(self, save_path: str) -> bool:
  class OpenDALFileStorage (line 875) | class OpenDALFileStorage(FileStorageInterface):
    method __init__ (line 876) | def __init__(self):
    method save_file (line 891) | async def save_file(self, file: UploadFile, save_path: str):
    method delete_file (line 896) | async def delete_file(self, file_code: FileCodes):
    method get_file_url (line 899) | async def get_file_url(self, file_code: FileCodes):
    method get_file_response (line 902) | async def get_file_response(self, file_code: FileCodes):
    method save_chunk (line 958) | async def save_chunk(self, upload_id: str, chunk_index: int, chunk_dat...
    method merge_chunks (line 963) | async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, ...
    method clean_chunks (line 1004) | async def clean_chunks(self, upload_id: str, save_path: str):
    method file_exists (line 1013) | async def file_exists(self, save_path: str) -> bool:
  class WebDAVFileStorage (line 1026) | class WebDAVFileStorage(FileStorageInterface):
    method __init__ (line 1029) | def __init__(self):
    method _build_url (line 1037) | def _build_url(self, path: str) -> str:
    method _mkdir_p (line 1041) | async def _mkdir_p(self, directory_path: str):
    method _is_dir_empty (line 1064) | async def _is_dir_empty(self, dir_path: str) -> bool:
    method _delete_empty_dirs (line 1076) | async def _delete_empty_dirs(self, file_path: str, session: aiohttp.Cl...
    method save_file (line 1092) | async def save_file(self, file: UploadFile, save_path: str):
    method delete_file (line 1132) | async def delete_file(self, file_code: FileCodes):
    method get_file_url (line 1154) | async def get_file_url(self, file_code: FileCodes):
    method get_file_response (line 1157) | async def get_file_response(self, file_code: FileCodes):
    method save_chunk (line 1210) | async def save_chunk(self, upload_id: str, chunk_index: int, chunk_dat...
    method merge_chunks (line 1228) | async def merge_chunks(self, upload_id: str, chunk_info: UploadChunk, ...
    method clean_chunks (line 1297) | async def clean_chunks(self, upload_id: str, save_path: str):
    method file_exists (line 1331) | async def file_exists(self, save_path: str) -> bool:

FILE: core/tasks.py
  function delete_expire_files (line 20) | async def delete_expire_files():
  function clean_incomplete_uploads (line 50) | async def clean_incomplete_uploads():

FILE: core/utils.py
  function get_random_num (line 16) | async def get_random_num():
  function get_random_string (line 27) | async def get_random_string():
  function get_now (line 35) | async def get_now():
  function get_select_token (line 43) | async def get_select_token(code: str):
  function get_file_url (line 55) | async def get_file_url(code: str):
  function max_save_times_desc (line 64) | async def max_save_times_desc(max_save_seconds: int):
  function hash_password (line 100) | def hash_password(password: str) -> str:
  function verify_password (line 110) | def verify_password(password: str, hashed: str) -> bool:
  function is_password_hashed (line 131) | def is_password_hashed(password: str) -> bool:
  function sanitize_filename (line 138) | async def sanitize_filename(filename: str) -> str:

FILE: docs/.vitepress/cache/deps/chunk-CQOUZRMK.js
  function makeMap (line 2) | function makeMap(str) {
  function normalizeStyle (line 104) | function normalizeStyle(value) {
  function parseStringStyle (line 124) | function parseStringStyle(cssText) {
  function stringifyStyle (line 134) | function stringifyStyle(styles) {
  function normalizeClass (line 147) | function normalizeClass(value) {
  function normalizeProps (line 167) | function normalizeProps(props) {
  function includeBooleanAttr (line 191) | function includeBooleanAttr(value) {
  function isRenderableAttrValue (line 203) | function isRenderableAttrValue(value) {
  function getEscapedCssVarName (line 211) | function getEscapedCssVarName(key, doubleEscape) {
  function looseCompareArrays (line 217) | function looseCompareArrays(a, b) {
  function looseEqual (line 225) | function looseEqual(a, b) {
  function looseIndexOf (line 263) | function looseIndexOf(arr, val) {
  function warn (line 306) | function warn(msg, ...args) {
  method constructor (line 311) | constructor(detached = false) {
  method active (line 324) | get active() {
  method pause (line 327) | pause() {
  method resume (line 344) | resume() {
  method run (line 360) | run(fn) {
  method on (line 377) | on() {
  method off (line 384) | off() {
  method stop (line 387) | stop(fromParent) {
  function effectScope (line 416) | function effectScope(detached) {
  function getCurrentScope (line 419) | function getCurrentScope() {
  function onScopeDispose (line 422) | function onScopeDispose(fn, failSilently = false) {
  method constructor (line 434) | constructor(fn) {
  method pause (line 446) | pause() {
  method resume (line 449) | resume() {
  method notify (line 461) | notify() {
  method run (line 469) | run() {
  method stop (line 494) | stop() {
  method trigger (line 505) | trigger() {
  method runIfDirty (line 517) | runIfDirty() {
  method dirty (line 522) | get dirty() {
  function batch (line 529) | function batch(sub, isComputed = false) {
  function startBatch (line 539) | function startBatch() {
  function endBatch (line 542) | function endBatch() {
  function prepareDeps (line 577) | function prepareDeps(sub) {
  function cleanupDeps (line 584) | function cleanupDeps(sub) {
  function isDirty (line 604) | function isDirty(sub) {
  function refreshComputed (line 615) | function refreshComputed(computed3) {
  function removeSub (line 651) | function removeSub(link, soft = false) {
  function removeDep (line 677) | function removeDep(link) {
  function effect (line 688) | function effect(fn, options) {
  function stop (line 706) | function stop(runner) {
  function pauseTracking (line 711) | function pauseTracking() {
  function resetTracking (line 715) | function resetTracking() {
  function cleanupEffect (line 719) | function cleanupEffect(e) {
  method constructor (line 734) | constructor(sub, dep) {
  method constructor (line 742) | constructor(computed3) {
  method track (line 754) | track(debugInfo) {
  method trigger (line 798) | trigger(debugInfo) {
  method notify (line 803) | notify(debugInfo) {
  function addSub (line 831) | function addSub(link) {
  function track (line 862) | function track(target, type, key) {
  function trigger (line 885) | function trigger(target, type, key, newValue, oldValue, oldTarget) {
  function getDepFromReactive (line 956) | function getDepFromReactive(object, key) {
  function reactiveReadArray (line 960) | function reactiveReadArray(array) {
  function shallowReadArray (line 966) | function shallowReadArray(arr) {
  method [Symbol.iterator] (line 972) | [Symbol.iterator]() {
  method concat (line 975) | concat(...args) {
  method entries (line 980) | entries() {
  method every (line 986) | every(fn, thisArg) {
  method filter (line 989) | filter(fn, thisArg) {
  method find (line 992) | find(fn, thisArg) {
  method findIndex (line 995) | findIndex(fn, thisArg) {
  method findLast (line 998) | findLast(fn, thisArg) {
  method findLastIndex (line 1001) | findLastIndex(fn, thisArg) {
  method forEach (line 1005) | forEach(fn, thisArg) {
  method includes (line 1008) | includes(...args) {
  method indexOf (line 1011) | indexOf(...args) {
  method join (line 1014) | join(separator) {
  method lastIndexOf (line 1018) | lastIndexOf(...args) {
  method map (line 1021) | map(fn, thisArg) {
  method pop (line 1024) | pop() {
  method push (line 1027) | push(...args) {
  method reduce (line 1030) | reduce(fn, ...args) {
  method reduceRight (line 1033) | reduceRight(fn, ...args) {
  method shift (line 1036) | shift() {
  method some (line 1040) | some(fn, thisArg) {
  method splice (line 1043) | splice(...args) {
  method toReversed (line 1046) | toReversed() {
  method toSorted (line 1049) | toSorted(comparer) {
  method toSpliced (line 1052) | toSpliced(...args) {
  method unshift (line 1055) | unshift(...args) {
  method values (line 1058) | values() {
  function iterator (line 1062) | function iterator(self2, method, wrapValue) {
  function apply (line 1078) | function apply(self2, method, fn, thisArg, wrappedRetFn, args) {
  function reduce (line 1101) | function reduce(self2, method, fn, args) {
  function searchProxy (line 1117) | function searchProxy(self2, method, args) {
  function noTracking (line 1127) | function noTracking(self2, method, args = []) {
  function hasOwnProperty2 (line 1139) | function hasOwnProperty2(key) {
  method constructor (line 1146) | constructor(_isReadonly = false, _isShallow = false) {
  method get (line 1150) | get(target, key, receiver) {
  method constructor (line 1204) | constructor(isShallow2 = false) {
  method set (line 1207) | set(target, key, value, receiver) {
  method deleteProperty (line 1240) | deleteProperty(target, key) {
  method has (line 1249) | has(target, key) {
  method ownKeys (line 1256) | ownKeys(target) {
  method constructor (line 1266) | constructor(isShallow2 = false) {
  method set (line 1269) | set(target, key) {
  method deleteProperty (line 1278) | deleteProperty(target, key) {
  function createIterableMethod (line 1294) | function createIterableMethod(method, isReadonly2, isShallow2) {
  function createReadonlyMethod (line 1324) | function createReadonlyMethod(type) {
  function createInstrumentations (line 1336) | function createInstrumentations(readonly2, shallow) {
  function createInstrumentationGetter (line 1475) | function createInstrumentationGetter(isReadonly2, shallow) {
  function checkIdentityKeys (line 1504) | function checkIdentityKeys(target, has, key) {
  function targetTypeMap (line 1517) | function targetTypeMap(rawType) {
  function getTargetType (line 1531) | function getTargetType(value) {
  function reactive (line 1534) | function reactive(target) {
  function shallowReactive (line 1546) | function shallowReactive(target) {
  function readonly (line 1555) | function readonly(target) {
  function shallowReadonly (line 1564) | function shallowReadonly(target) {
  function createReactiveObject (line 1573) | function createReactiveObject(target, isReadonly2, baseHandlers, collect...
  function isReactive (line 1602) | function isReactive(value) {
  function isReadonly (line 1608) | function isReadonly(value) {
  function isShallow (line 1611) | function isShallow(value) {
  function isProxy (line 1614) | function isProxy(value) {
  function toRaw (line 1617) | function toRaw(observed) {
  function markRaw (line 1621) | function markRaw(value) {
  function isRef2 (line 1629) | function isRef2(r) {
  function ref (line 1632) | function ref(value) {
  function shallowRef (line 1635) | function shallowRef(value) {
  function createRef (line 1638) | function createRef(rawValue, shallow) {
  method constructor (line 1645) | constructor(value, isShallow2) {
  method value (line 1653) | get value() {
  method value (line 1665) | set value(newValue) {
  function triggerRef (line 1686) | function triggerRef(ref2) {
  function unref (line 1700) | function unref(ref2) {
  function toValue (line 1703) | function toValue(source) {
  function proxyRefs (line 1718) | function proxyRefs(objectWithRefs) {
  method constructor (line 1722) | constructor(factory) {
  method value (line 1730) | get value() {
  method value (line 1733) | set value(newVal) {
  function customRef (line 1737) | function customRef(factory) {
  function toRefs (line 1740) | function toRefs(object) {
  method constructor (line 1751) | constructor(_object, _key, _defaultValue) {
  method value (line 1758) | get value() {
  method value (line 1762) | set value(newVal) {
  method dep (line 1765) | get dep() {
  method constructor (line 1770) | constructor(_getter) {
  method value (line 1776) | get value() {
  function toRef (line 1780) | function toRef(source, key, defaultValue) {
  function propertyToRef (line 1791) | function propertyToRef(source, key, defaultValue) {
  method constructor (line 1796) | constructor(fn, setter, isSSR) {
  method notify (line 1814) | notify() {
  method value (line 1822) | get value() {
  method value (line 1834) | set value(newValue) {
  function computed (line 1842) | function computed(getterOrOptions, debugOptions, isSSR = false) {
  function getCurrentWatcher (line 1872) | function getCurrentWatcher() {
  function onWatcherCleanup (line 1875) | function onWatcherCleanup(cleanupFn, failSilently = false, owner = activ...
  function watch (line 1886) | function watch(source, cb, options = EMPTY_OBJ) {
  function traverse (line 2042) | function traverse(value, depth = Infinity, seen) {
  function pushWarningContext (line 2077) | function pushWarningContext(vnode) {
  function popWarningContext (line 2080) | function popWarningContext() {
  function warn$1 (line 2084) | function warn$1(msg, ...args) {
  function getComponentTrace (line 2121) | function getComponentTrace() {
  function formatTrace (line 2142) | function formatTrace(trace) {
  function formatTraceEntry (line 2150) | function formatTraceEntry({ vnode, recurseCount }) {
  function formatProps (line 2161) | function formatProps(props) {
  function formatProp (line 2172) | function formatProp(key, value, raw) {
  function assertNumber (line 2188) | function assertNumber(val, type) {
  function callWithErrorHandling (line 2261) | function callWithErrorHandling(fn, instance, type, args) {
  function callWithAsyncErrorHandling (line 2268) | function callWithAsyncErrorHandling(fn, instance, type, args) {
  function handleError (line 2290) | function handleError(err, instance, type, throwInDev = true) {
  function logError (line 2321) | function logError(err, type, contextVNode, throwInDev = true, throwInPro...
  function nextTick (line 2350) | function nextTick(fn) {
  function findInsertionIndex (line 2354) | function findInsertionIndex(id) {
  function queueJob (line 2369) | function queueJob(job) {
  function queueFlush (line 2383) | function queueFlush() {
  function queuePostFlushCb (line 2388) | function queuePostFlushCb(cb) {
  function flushPreFlushCbs (line 2401) | function flushPreFlushCbs(instance, seen, i = flushIndex + 1) {
  function flushPostFlushCbs (line 2426) | function flushPostFlushCbs(seen) {
  function flushJobs (line 2456) | function flushJobs(seen) {
  function checkRecursiveUpdates (line 2497) | function checkRecursiveUpdates(seen, fn) {
  function registerHMR (line 2522) | function registerHMR(instance) {
  function unregisterHMR (line 2531) | function unregisterHMR(instance) {
  function createRecord (line 2534) | function createRecord(id, initialDef) {
  function normalizeClassComponent (line 2544) | function normalizeClassComponent(component) {
  function rerender (line 2547) | function rerender(id, newRender) {
  function reload (line 2564) | function reload(id, newComp) {
  function updateComponentDef (line 2612) | function updateComponentDef(oldComp, newComp) {
  function tryWrap (line 2620) | function tryWrap(fn) {
  function emit$1 (line 2635) | function emit$1(event, ...args) {
  function setDevtoolsHook$1 (line 2642) | function setDevtoolsHook$1(hook, target) {
  function devtoolsInitApp (line 2674) | function devtoolsInitApp(app, version2) {
  function devtoolsUnmountApp (line 2682) | function devtoolsUnmountApp(app) {
  function createDevtoolsComponentHook (line 2703) | function createDevtoolsComponentHook(hook) {
  function createDevtoolsPerformanceHook (line 2722) | function createDevtoolsPerformanceHook(hook) {
  function devtoolsComponentEmit (line 2727) | function devtoolsComponentEmit(component, event, params) {
  function setCurrentRenderingInstance (line 2738) | function setCurrentRenderingInstance(instance) {
  function pushScopeId (line 2744) | function pushScopeId(id) {
  function popScopeId (line 2747) | function popScopeId() {
  function withCtx (line 2751) | function withCtx(fn, ctx = currentRenderingInstance, isNonScopedSlot) {
  function validateDirectiveName (line 2780) | function validateDirectiveName(name) {
  function withDirectives (line 2785) | function withDirectives(vnode, directives) {
  function invokeDirectiveHook (line 2816) | function invokeDirectiveHook(vnode, prevVNode, instance, name) {
  method process (line 2870) | process(n1, n2, container, anchor, parentComponent, parentSuspense, name...
  method remove (line 3042) | remove(vnode, parentComponent, parentSuspense, { um: unmount, o: { remov...
  function moveTeleport (line 3074) | function moveTeleport(vnode, container, parentAnchor, { o: { insert }, m...
  function hydrateTeleport (line 3099) | function hydrateTeleport(node, vnode, parentComponent, parentSuspense, s...
  function updateCssVars (line 3156) | function updateCssVars(vnode, isDisabled) {
  function prepareAnchor (line 3174) | function prepareAnchor(target, vnode, createText, insert) {
  function useTransitionState (line 3186) | function useTransitionState() {
  method setup (line 3229) | setup(props, { slots }) {
  function findNonCommentChild (line 3310) | function findNonCommentChild(children) {
  function getLeavingNodesForType (line 3331) | function getLeavingNodesForType(state, vnode) {
  function resolveTransitionHooks (line 3340) | function resolveTransitionHooks(vnode, props, state, instance, postClone) {
  function emptyPlaceholder (line 3482) | function emptyPlaceholder(vnode) {
  function getInnerChild$1 (line 3489) | function getInnerChild$1(vnode) {
  function setTransitionHooks (line 3509) | function setTransitionHooks(vnode, hooks) {
  function getTransitionRawChildren (line 3520) | function getTransitionRawChildren(children, keepComment = false, parentK...
  function defineComponent (line 3542) | function defineComponent(options, extraOptions) {
  function useId (line 3549) | function useId() {
  function markAsyncBoundary (line 3560) | function markAsyncBoundary(instance) {
  function useTemplateRef (line 3564) | function useTemplateRef(key) {
  function setRef (line 3590) | function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = fa...
  function createHydrationFunctions (line 3711) | function createHydrationFunctions(rendererInternals) {
  function propHasMismatch (line 4207) | function propHasMismatch(el, key, clientValue, vnode, instance) {
  function toClassSet (line 4275) | function toClassSet(str) {
  function isSetEqual (line 4278) | function isSetEqual(a, b) {
  function toStyleMap (line 4289) | function toStyleMap(str) {
  function isMapEqual (line 4301) | function isMapEqual(a, b) {
  function resolveCssVars (line 4312) | function resolveCssVars(instance, vnode, expectedMap) {
  function isMismatchAllowed (line 4350) | function isMismatchAllowed(el, allowedType) {
  function elementIsVisibleInViewport (line 4375) | function elementIsVisibleInViewport(el) {
  function forEachElement (line 4436) | function forEachElement(node, cb) {
  function defineAsyncComponent (line 4460) | function defineAsyncComponent(source) {
  function createInnerComp (line 4604) | function createInnerComp(comp, parent) {
  method setup (line 4624) | setup(props, { slots }) {
  function matches (line 4817) | function matches(pattern, name) {
  function onActivated (line 4828) | function onActivated(hook, target) {
  function onDeactivated (line 4831) | function onDeactivated(hook, target) {
  function registerKeepAliveHook (line 4834) | function registerKeepAliveHook(hook, type, target = currentInstance) {
  function injectToKeepAliveRoot (line 4856) | function injectToKeepAliveRoot(hook, type, target, keepAliveRoot) {
  function resetShapeFlag (line 4868) | function resetShapeFlag(vnode) {
  function getInnerChild (line 4872) | function getInnerChild(vnode) {
  function injectHook (line 4875) | function injectHook(type, hook, target = currentInstance, prepend = fals...
  function onErrorCaptured (line 4919) | function onErrorCaptured(hook, target = currentInstance) {
  function resolveComponent (line 4924) | function resolveComponent(name, maybeSelfReference) {
  function resolveDynamicComponent (line 4928) | function resolveDynamicComponent(component) {
  function resolveDirective (line 4935) | function resolveDirective(name) {
  function resolveAsset (line 4938) | function resolveAsset(type, name, warnMissing = true, maybeSelfReference...
  function resolve (line 4972) | function resolve(registry, name) {
  function renderList (line 4975) | function renderList(source, renderItem, cache, index) {
  function createSlots (line 5025) | function createSlots(slots, dynamicSlots) {
  function renderSlot (line 5042) | function renderSlot(slots, name, props = {}, fallback, noSlotted) {
  function ensureValidVNode (line 5084) | function ensureValidVNode(vnodes) {
  function toHandlers (line 5093) | function toHandlers(obj, preserveCaseIfNecessary) {
  method get (line 5135) | get({ _: instance }, key) {
  method set (line 5218) | set({ _: instance }, key, value) {
  method has (line 5251) | has({
  method defineProperty (line 5257) | defineProperty(target, key, descriptor) {
  method get (line 5275) | get(target, key) {
  method has (line 5281) | has(_, key) {
  function createDevRenderContext (line 5293) | function createDevRenderContext(instance) {
  function exposePropsOnRenderContext (line 5312) | function exposePropsOnRenderContext(instance) {
  function exposeSetupStateOnRenderContext (line 5328) | function exposeSetupStateOnRenderContext(instance) {
  function defineProps (line 5352) | function defineProps() {
  function defineEmits (line 5358) | function defineEmits() {
  function defineExpose (line 5364) | function defineExpose(exposed) {
  function defineOptions (line 5369) | function defineOptions(options) {
  function defineSlots (line 5374) | function defineSlots() {
  function defineModel (line 5380) | function defineModel() {
  function withDefaults (line 5385) | function withDefaults(props, defaults) {
  function useSlots (line 5391) | function useSlots() {
  function useAttrs (line 5394) | function useAttrs() {
  function getContext (line 5397) | function getContext() {
  function normalizePropsOrEmits (line 5404) | function normalizePropsOrEmits(props) {
  function mergeDefaults (line 5410) | function mergeDefaults(raw, defaults) {
  function mergeModels (line 5432) | function mergeModels(a, b) {
  function createPropsRestProxy (line 5437) | function createPropsRestProxy(props, excludedKeys) {
  function withAsyncContext (line 5449) | function withAsyncContext(getAwaitable) {
  function createDuplicateChecker (line 5466) | function createDuplicateChecker() {
  function applyOptions (line 5477) | function applyOptions(instance) {
  function resolveInjections (line 5671) | function resolveInjections(injectOptions, ctx, checkDuplicateProperties ...
  function callHook (line 5706) | function callHook(hook, instance, type) {
  function createWatcher (line 5713) | function createWatcher(raw, ctx, publicThis, key) {
  function resolveMergedOptions (line 5743) | function resolveMergedOptions(instance) {
  function mergeOptions (line 5773) | function mergeOptions(to, from, strats, asMixin = false) {
  function mergeDataFn (line 5826) | function mergeDataFn(to, from) {
  function mergeInject (line 5840) | function mergeInject(to, from) {
  function normalizeInject (line 5843) | function normalizeInject(raw) {
  function mergeAsArray (line 5853) | function mergeAsArray(to, from) {
  function mergeObjectOptions (line 5856) | function mergeObjectOptions(to, from) {
  function mergeEmitsOrPropsOptions (line 5859) | function mergeEmitsOrPropsOptions(to, from) {
  function mergeWatchOptions (line 5873) | function mergeWatchOptions(to, from) {
  function createAppContext (line 5882) | function createAppContext() {
  function createAppAPI (line 5904) | function createAppAPI(render2, hydrate2) {
  function provide (line 6083) | function provide(key, value) {
  function inject (line 6097) | function inject(key, defaultValue, treatDefaultAsFactory = false) {
  function hasInjectionContext (line 6112) | function hasInjectionContext() {
  function initProps (line 6118) | function initProps(instance, rawProps, isStateful, isSSR = false) {
  function isInHmrContext (line 6142) | function isInHmrContext(instance) {
  function updateProps (line 6148) | function updateProps(instance, rawProps, rawPrevProps, optimized) {
  function setFullProps (line 6240) | function setFullProps(instance, rawProps, props, attrs) {
  function resolvePropValue (line 6282) | function resolvePropValue(options, props, key, value, instance, isAbsent) {
  function normalizePropsOptions (line 6324) | function normalizePropsOptions(comp, appContext, asMixin = false) {
  function validatePropName (line 6413) | function validatePropName(key) {
  function getType (line 6421) | function getType(ctor) {
  function validateProps (line 6433) | function validateProps(rawProps, props, instance) {
  function validateProp (line 6449) | function validateProp(name, value, prop, props, isAbsent) {
  function assertType (line 6479) | function assertType(value, type) {
  function getInvalidTypeMessage (line 6502) | function getInvalidTypeMessage(name, value, expectedTypes) {
  function styleValue (line 6520) | function styleValue(value, type) {
  function isExplicable (line 6529) | function isExplicable(type) {
  function isBoolean (line 6533) | function isBoolean(...args) {
  function startMeasure (line 6637) | function startMeasure(instance, type) {
  function endMeasure (line 6645) | function endMeasure(instance, type) {
  function isSupported (line 6662) | function isSupported() {
  function initFeatureFlags (line 6674) | function initFeatureFlags() {
  function createRenderer (line 6698) | function createRenderer(options) {
  function createHydrationRenderer (line 6701) | function createHydrationRenderer(options) {
  function baseCreateRenderer (line 6704) | function baseCreateRenderer(options, createHydrationFns) {
  function resolveChildrenNamespace (line 8106) | function resolveChildrenNamespace({ type, props }, currentNamespace) {
  function toggleRecurse (line 8109) | function toggleRecurse({ effect: effect2, job }, allowed) {
  function needTransition (line 8118) | function needTransition(parentSuspense, transition) {
  function traverseStaticChildren (line 8121) | function traverseStaticChildren(n1, n2, shallow = false) {
  function getSequence (line 8145) | function getSequence(arr) {
  function locateNonHydratedAsyncRoot (line 8185) | function locateNonHydratedAsyncRoot(instance) {
  function invalidateMount (line 8195) | function invalidateMount(hooks) {
  function watchEffect (line 8213) | function watchEffect(effect2, options) {
  function watchPostEffect (line 8216) | function watchPostEffect(effect2, options) {
  function watchSyncEffect (line 8223) | function watchSyncEffect(effect2, options) {
  function watch2 (line 8230) | function watch2(source, cb, options) {
  function doWatch (line 8238) | function doWatch(source, cb, options = EMPTY_OBJ) {
  function instanceWatch (line 8313) | function instanceWatch(source, value, options) {
  function createPathGetter (line 8328) | function createPathGetter(ctx, path) {
  function useModel (line 8338) | function useModel(props, name, options = EMPTY_OBJ) {
  function emit (line 8404) | function emit(instance, event, ...rawArgs) {
  function normalizeEmitsOptions (line 8489) | function normalizeEmitsOptions(comp, appContext, asMixin = false) {
  function isEmitListener (line 8532) | function isEmitListener(options, key) {
  function markAttrsAccessed (line 8540) | function markAttrsAccessed() {
  function renderComponentRoot (line 8543) | function renderComponentRoot(instance) {
  function filterSingleRoot (line 8712) | function filterSingleRoot(children, recurse = true) {
  function shouldUpdateComponent (line 8754) | function shouldUpdateComponent(prevVNode, nextVNode, optimized) {
  function hasPropsChanged (line 8801) | function hasPropsChanged(prevProps, nextProps, emitsOptions) {
  function updateHOCHostEl (line 8814) | function updateHOCHostEl({ vnode, parent }, el) {
  method process (line 8837) | process(n1, n2, container, anchor, parentComponent, parentSuspense, name...
  function triggerEvent (line 8874) | function triggerEvent(vnode, name) {
  function mountSuspense (line 8880) | function mountSuspense(vnode, container, anchor, parentComponent, parent...
  function patchSuspense (line 8927) | function patchSuspense(n1, n2, container, anchor, parentComponent, names...
  function createSuspenseBoundary (line 9088) | function createSuspenseBoundary(vnode, parentSuspense, parentComponent, ...
  function hydrateSuspense (line 9327) | function hydrateSuspense(node, vnode, parentComponent, parentSuspense, n...
  function normalizeSuspenseChildren (line 9355) | function normalizeSuspenseChildren(vnode) {
  function normalizeSuspenseSlot (line 9363) | function normalizeSuspenseSlot(s) {
  function queueEffectWithSuspense (line 9391) | function queueEffectWithSuspense(fn, suspense) {
  function setActiveBranch (line 9402) | function setActiveBranch(suspense, branch) {
  function isVNodeSuspensible (line 9416) | function isVNodeSuspensible(vnode) {
  function openBlock (line 9426) | function openBlock(disableTracking = false) {
  function closeBlock (line 9429) | function closeBlock() {
  function setBlockTracking (line 9434) | function setBlockTracking(value, inVOnce = false) {
  function setupBlock (line 9440) | function setupBlock(vnode) {
  function createElementBlock (line 9448) | function createElementBlock(type, props, children, patchFlag, dynamicPro...
  function createBlock (line 9461) | function createBlock(type, props, children, patchFlag, dynamicProps) {
  function isVNode (line 9473) | function isVNode(value) {
  function isSameVNodeType (line 9476) | function isSameVNodeType(n1, n2) {
  function transformVNodeArgs (line 9488) | function transformVNodeArgs(transformer) {
  function createBaseVNode (line 9507) | function createBaseVNode(type, props = null, children = null, patchFlag ...
  function _createVNode (line 9562) | function _createVNode(type, props = null, children = null, patchFlag = 0...
  function guardReactiveProps (line 9626) | function guardReactiveProps(props) {
  function cloneVNode (line 9630) | function cloneVNode(vnode, extraProps, mergeRef = false, cloneTransition...
  function deepCloneVNode (line 9684) | function deepCloneVNode(vnode) {
  function createTextVNode (line 9691) | function createTextVNode(text = " ", flag = 0) {
  function createStaticVNode (line 9694) | function createStaticVNode(content, numberOfNodes) {
  function createCommentVNode (line 9699) | function createCommentVNode(text = "", asBlock = false) {
  function normalizeVNode (line 9702) | function normalizeVNode(child) {
  function cloneIfMounted (line 9718) | function cloneIfMounted(child) {
  function normalizeChildren (line 9721) | function normalizeChildren(vnode, children) {
  function mergeProps (line 9766) | function mergeProps(...args) {
  function invokeVNodeHook (line 9790) | function invokeVNodeHook(hook, instance, vnode, prevVNode = null) {
  function createComponentInstance (line 9798) | function createComponentInstance(vnode, parent, suspense) {
  function validateComponentName (line 9927) | function validateComponentName(name, { isNativeTag }) {
  function isStatefulComponent (line 9934) | function isStatefulComponent(instance) {
  function setupComponent (line 9938) | function setupComponent(instance, isSSR = false, optimized = false) {
  function setupStatefulComponent (line 9948) | function setupStatefulComponent(instance, isSSR) {
  function handleSetupResult (line 10022) | function handleSetupResult(instance, setupResult, isSSR) {
  function registerRuntimeCompiler (line 10051) | function registerRuntimeCompiler(_compile) {
  function finishComponentSetup (line 10060) | function finishComponentSetup(instance, isSSR, skipOptions) {
  method get (line 10113) | get(target, key) {
  method set (line 10118) | set() {
  method deleteProperty (line 10122) | deleteProperty() {
  method get (line 10127) | get(target, key) {
  function getSlotsProxy (line 10132) | function getSlotsProxy(instance) {
  function createSetupContext (line 10140) | function createSetupContext(instance) {
  function getComponentPublicInstance (line 10188) | function getComponentPublicInstance(instance) {
  function getComponentName (line 10208) | function getComponentName(Component, includeInferred = true) {
  function formatComponentName (line 10211) | function formatComponentName(instance, Component, isRoot = false) {
  function isClassComponent (line 10233) | function isClassComponent(value) {
  function h (line 10246) | function h(type, propsOrChildren, children) {
  function initCustomFormatter (line 10266) | function initCustomFormatter() {
  function withMemo (line 10443) | function withMemo(memo, render2, cache, index) {
  function isMemoSame (line 10453) | function isMemoSame(cached, memo) {
  method setScopeId (line 10535) | setScopeId(el, id) {
  method insertStaticContent (line 10542) | insertStaticContent(content, parent, anchor, namespace, start, end) {
  function resolveTransitionProps (line 10615) | function resolveTransitionProps(rawProps) {
  function normalizeDuration (line 10729) | function normalizeDuration(duration) {
  function NumberOf (line 10739) | function NumberOf(val) {
  function addTransitionClass (line 10746) | function addTransitionClass(el, cls) {
  function removeTransitionClass (line 10750) | function removeTransitionClass(el, cls) {
  function nextFrame (line 10760) | function nextFrame(cb) {
  function whenTransitionEnds (line 10766) | function whenTransitionEnds(el, expectedType, explicitTimeout, resolve2) {
  function getTransitionInfo (line 10798) | function getTransitionInfo(el, expectedType) {
  function getTimeout (line 10837) | function getTimeout(delays, durations) {
  function toMs (line 10843) | function toMs(s) {
  function forceReflow (line 10847) | function forceReflow() {
  function patchClass (line 10850) | function patchClass(el, value, isSVG) {
  method beforeMount (line 10866) | beforeMount(el, { value }, { transition }) {
  method mounted (line 10874) | mounted(el, { value }, { transition }) {
  method updated (line 10879) | updated(el, { value, oldValue }, { transition }) {
  method beforeUnmount (line 10895) | beforeUnmount(el, { value }) {
  function setDisplay (line 10902) | function setDisplay(el, value) {
  function initVShowForSSR (line 10906) | function initVShowForSSR() {
  function useCssVars (line 10914) | function useCssVars(getter) {
  function setVarsOnVNode (line 10947) | function setVarsOnVNode(vnode, vars) {
  function setVarsOnNode (line 10973) | function setVarsOnNode(el, vars) {
  function patchStyle (line 10985) | function patchStyle(el, prev, next) {
  function setStyle (line 11035) | function setStyle(style, name, val) {
  function autoPrefix (line 11065) | function autoPrefix(style, rawName) {
  function patchAttr (line 11084) | function patchAttr(el, key, value, isSVG, instance, isBoolean2 = isSpeci...
  function patchDOMProp (line 11102) | function patchDOMProp(el, key, value, parentComponent, attrName) {
  function addEventListener (line 11152) | function addEventListener(el, event, handler, options) {
  function removeEventListener (line 11155) | function removeEventListener(el, event, handler, options) {
  function patchEvent (line 11159) | function patchEvent(el, rawName, prevValue, nextValue, instance = null) {
  function parseName (line 11179) | function parseName(name) {
  function createInvoker (line 11195) | function createInvoker(initialValue, instance) {
  function sanitizeEventValue (line 11213) | function sanitizeEventValue(value, propName) {
  function patchStopImmediatePropagation (line 11223) | function patchStopImmediatePropagation(e, value) {
  function shouldSetAsProp (line 11268) | function shouldSetAsProp(el, key, value, isSVG) {
  function defineCustomElement (line 11302) | function defineCustomElement(options, extraOptions, _createApp) {
  method constructor (line 11319) | constructor(_def, _props = {}, _createApp = createApp) {
  method connectedCallback (line 11352) | connectedCallback() {
  method _setParent (line 11381) | _setParent(parent = this._parent) {
  method disconnectedCallback (line 11387) | disconnectedCallback() {
  method _resolveDef (line 11404) | _resolveDef() {
  method _mount (line 11455) | _mount(def2) {
  method _resolveProps (line 11478) | _resolveProps(def2) {
  method _setAttr (line 11497) | _setAttr(key) {
  method _getProp (line 11510) | _getProp(key) {
  method _setProp (line 11516) | _setProp(key, val, shouldReflect = true, shouldUpdate = false) {
  method _update (line 11543) | _update() {
  method _createVNode (line 11546) | _createVNode() {
  method _applyStyles (line 11587) | _applyStyles(styles, owner) {
  method _parseSlots (line 11620) | _parseSlots() {
  method _renderSlots (line 11632) | _renderSlots() {
  method _injectChildStyle (line 11662) | _injectChildStyle(comp) {
  method _removeChildStyle (line 11668) | _removeChildStyle(comp) {
  function useHost (line 11681) | function useHost(caller) {
  function useShadowRoot (line 11699) | function useShadowRoot() {
  function useCssModule (line 11703) | function useCssModule(name = "$style") {
  method setup (line 11737) | setup(props, { slots }) {
  function callPendingCbs (line 11819) | function callPendingCbs(c) {
  function recordPosition (line 11828) | function recordPosition(c) {
  function applyTranslation (line 11831) | function applyTranslation(c) {
  function hasCSSTransform (line 11843) | function hasCSSTransform(el, root, moveClass) {
  function onCompositionStart (line 11863) | function onCompositionStart(e) {
  function onCompositionEnd (line 11866) | function onCompositionEnd(e) {
  method created (line 11875) | created(el, { modifiers: { lazy, trim, number } }, vnode) {
  method mounted (line 11901) | mounted(el, { value }) {
  method beforeUpdate (line 11904) | beforeUpdate(el, { value, oldValue, modifiers: { lazy, trim, number } },...
  method created (line 11926) | created(el, _, vnode) {
  method beforeUpdate (line 11958) | beforeUpdate(el, binding, vnode) {
  function setChecked (line 11963) | function setChecked(el, { value, oldValue }, vnode) {
  method created (line 11979) | created(el, { value }, vnode) {
  method beforeUpdate (line 11986) | beforeUpdate(el, { value, oldValue }, vnode) {
  method created (line 11996) | created(el, { value, modifiers: { number } }, vnode) {
  method mounted (line 12014) | mounted(el, { value }) {
  method beforeUpdate (line 12017) | beforeUpdate(el, _binding, vnode) {
  method updated (line 12020) | updated(el, { value }) {
  function setSelected (line 12026) | function setSelected(el, value) {
  function getValue (line 12058) | function getValue(el) {
  function getCheckboxValue (line 12061) | function getCheckboxValue(el, checked) {
  method created (line 12066) | created(el, binding, vnode) {
  method mounted (line 12069) | mounted(el, binding, vnode) {
  method beforeUpdate (line 12072) | beforeUpdate(el, binding, vnode, prevVNode) {
  method updated (line 12075) | updated(el, binding, vnode, prevVNode) {
  function resolveDynamicModel (line 12079) | function resolveDynamicModel(tagName, type) {
  function callModelHook (line 12096) | function callModelHook(el, binding, vnode, prevVNode, hook) {
  function initVModelForSSR (line 12104) | function initVModelForSSR() {
  function ensureRenderer (line 12190) | function ensureRenderer() {
  function ensureHydrationRenderer (line 12193) | function ensureHydrationRenderer() {
  function resolveRootNamespace (line 12245) | function resolveRootNamespace(container) {
  function injectNativeTagCheck (line 12253) | function injectNativeTagCheck(app) {
  function injectCompilerOptionsCheck (line 12259) | function injectCompilerOptionsCheck(app) {
  function normalizeContainer (line 12288) | function normalizeContainer(container) {
  function initDev (line 12315) | function initDev() {

FILE: docs/.vitepress/cache/deps/chunk-KT7LHMJ2.js
  function computedEager (line 41) | function computedEager(fn, options) {
  function computedWithControl (line 52) | function computedWithControl(source, fn) {
  function tryOnScopeDispose (line 85) | function tryOnScopeDispose(fn) {
  function createEventHook (line 92) | function createEventHook() {
  function createGlobalState (line 118) | function createGlobalState(stateFactory) {
  function createInjectionState (line 152) | function createInjectionState(composable, options) {
  function createSharedComposable (line 163) | function createSharedComposable(composable) {
  function extendRef (line 185) | function extendRef(ref2, extend, { enumerable = false, unwrap = true } =...
  function get (line 205) | function get(obj, key) {
  function isDefined (line 210) | function isDefined(v) {
  function makeDestructurable (line 213) | function makeDestructurable(obj, arr) {
  function reactify (line 233) | function reactify(fn, options) {
  function reactifyObject (line 239) | function reactifyObject(obj, optionsOrKeys = {}) {
  function toReactive (line 261) | function toReactive(objectRef) {
  function reactiveComputed (line 293) | function reactiveComputed(fn) {
  function reactiveOmit (line 296) | function reactiveOmit(obj, ...keys2) {
  function getIsIOS (line 323) | function getIsIOS() {
  function createFilterWrapper (line 327) | function createFilterWrapper(filter, fn) {
  function debounceFilter (line 338) | function debounceFilter(ms, options = {}) {
  function throttleFilter (line 381) | function throttleFilter(...args) {
  function pausableFilter (line 435) | function pausableFilter(extendFilter = bypassFilter) {
  function cacheStringFunction (line 449) | function cacheStringFunction(fn) {
  function promiseTimeout (line 462) | function promiseTimeout(ms, throwOnTimeout = false, reason = "Timeout") {
  function identity (line 470) | function identity(arg) {
  function createSingletonPromise (line 473) | function createSingletonPromise(fn) {
  function invoke (line 488) | function invoke(fn) {
  function containsProp (line 491) | function containsProp(obj, ...props) {
  function increaseWithUnit (line 494) | function increaseWithUnit(target, delta) {
  function pxValue (line 505) | function pxValue(px) {
  function objectPick (line 508) | function objectPick(obj, keys2, omitUndefined = false) {
  function objectOmit (line 517) | function objectOmit(obj, keys2, omitUndefined = false) {
  function objectEntries (line 522) | function objectEntries(obj) {
  function getLifeCycleTarget (line 525) | function getLifeCycleTarget(target) {
  function toArray (line 528) | function toArray(value) {
  function toRef2 (line 531) | function toRef2(...args) {
  function reactivePick (line 538) | function reactivePick(obj, ...keys2) {
  function refAutoReset (line 543) | function refAutoReset(defaultValue, afterMs = 1e4) {
  function useDebounceFn (line 568) | function useDebounceFn(fn, ms = 200, options = {}) {
  function refDebounced (line 574) | function refDebounced(value, ms = 200, options = {}) {
  function refDefault (line 582) | function refDefault(source, defaultValue) {
  function useThrottleFn (line 593) | function useThrottleFn(fn, ms = 200, trailing = false, leading = true, r...
  function refThrottled (line 599) | function refThrottled(value, delay = 200, trailing = true, leading = tru...
  function refWithControl (line 609) | function refWithControl(initial, options = {}) {
  function set (line 660) | function set(...args) {
  function watchWithFilter (line 670) | function watchWithFilter(source, cb, options = {}) {
  function watchPausable (line 684) | function watchPausable(source, cb, options = {}) {
  function syncRef (line 700) | function syncRef(left, right, ...[options]) {
  function syncRefs (line 738) | function syncRefs(source, targets, options = {}) {
  function toRefs2 (line 751) | function toRefs2(objectRef, options = {}) {
  function tryOnBeforeMount (line 783) | function tryOnBeforeMount(fn, sync = true, target) {
  function tryOnBeforeUnmount (line 792) | function tryOnBeforeUnmount(fn, target) {
  function tryOnMounted (line 797) | function tryOnMounted(fn, sync = true, target) {
  function tryOnUnmounted (line 806) | function tryOnUnmounted(fn, target) {
  function createUntil (line 811) | function createUntil(r, isNot = false) {
  function until (line 932) | function until(r) {
  function defaultComparator (line 935) | function defaultComparator(value, othVal) {
  function useArrayDifference (line 938) | function useArrayDifference(...args) {
  function useArrayEvery (line 958) | function useArrayEvery(list, fn) {
  function useArrayFilter (line 961) | function useArrayFilter(list, fn) {
  function useArrayFind (line 964) | function useArrayFind(list, fn) {
  function useArrayFindIndex (line 969) | function useArrayFindIndex(list, fn) {
  function findLast (line 972) | function findLast(arr, cb) {
  function useArrayFindLast (line 980) | function useArrayFindLast(list, fn) {
  function isArrayIncludesOptions (line 985) | function isArrayIncludesOptions(obj) {
  function useArrayIncludes (line 988) | function useArrayIncludes(...args) {
  function useArrayJoin (line 1010) | function useArrayJoin(list, separator) {
  function useArrayMap (line 1013) | function useArrayMap(list, fn) {
  function useArrayReduce (line 1016) | function useArrayReduce(list, reducer, ...args) {
  function useArraySome (line 1023) | function useArraySome(list, fn) {
  function uniq (line 1026) | function uniq(array) {
  function uniqueElementsBy (line 1029) | function uniqueElementsBy(array, fn) {
  function useArrayUnique (line 1036) | function useArrayUnique(list, compareFn) {
  function useCounter (line 1042) | function useCounter(initialValue = 0, options = {}) {
  function defaultMeridiem (line 1061) | function defaultMeridiem(hours, minutes, isLowercase, hasPeriod) {
  function formatOrdinal (line 1067) | function formatOrdinal(num) {
  function formatDate (line 1072) | function formatDate(date, formatStr, options = {}) {
  function normalizeDate (line 1122) | function normalizeDate(date) {
  function useDateFormat (line 1139) | function useDateFormat(date, formatStr = "HH:mm:ss", options = {}) {
  function useIntervalFn (line 1142) | function useIntervalFn(cb, interval = 1e3, options = {}) {
  function useInterval (line 1186) | function useInterval(interval = 1e3, options = {}) {
  function useLastChanged (line 1215) | function useLastChanged(source, options = {}) {
  function useTimeoutFn (line 1225) | function useTimeoutFn(cb, interval, options = {}) {
  function useTimeout (line 1262) | function useTimeout(interval = 1e3, options = {}) {
  function useToNumber (line 1282) | function useToNumber(value, options = {}) {
  function useToString (line 1299) | function useToString(value) {
  function useToggle (line 1302) | function useToggle(initialValue = false, options = {}) {
  function watchArray (line 1324) | function watchArray(source, cb, options) {
  function watchAtMost (line 1346) | function watchAtMost(source, cb, options) {
  function watchDebounced (line 1364) | function watchDebounced(source, cb, options = {}) {
  function watchDeep (line 1379) | function watchDeep(source, cb, options) {
  function watchIgnorable (line 1389) | function watchIgnorable(source, cb, options = {}) {
  function watchImmediate (line 1459) | function watchImmediate(source, cb, options) {
  function watchOnce (line 1469) | function watchOnce(source, cb, options) {
  function watchThrottled (line 1476) | function watchThrottled(source, cb, options = {}) {
  function watchTriggerable (line 1492) | function watchTriggerable(source, cb, options = {}) {
  function getWatchSources (line 1522) | function getWatchSources(sources) {
  function getOldValue (line 1529) | function getOldValue(source) {
  function whenever (line 1532) | function whenever(source, cb, options) {
  function computedAsync (line 1551) | function computedAsync(evaluationCallback, initialState, optionsOrRef) {
  function computedInject (line 1608) | function computedInject(key, options, defaultSource, treatDefaultAsFacto...
  function createReusableTemplate (line 1623) | function createReusableTemplate(options = {}) {
  function keysToCamelKebabCase (line 1652) | function keysToCamelKebabCase(obj) {
  function createTemplatePromise (line 1658) | function createTemplatePromise(options = {}) {
  function createUnrefFn (line 1705) | function createUnrefFn(fn) {
  function unrefElement (line 1714) | function unrefElement(elRef) {
  function useEventListener (line 1719) | function useEventListener(...args) {
  function onClickOutside (line 1767) | function onClickOutside(target, handler, options = {}) {
  function useMounted (line 1843) | function useMounted() {
  function useSupported (line 1853) | function useSupported(callback) {
  function useMutationObserver (line 1860) | function useMutationObserver(target, callback, options = {}) {
  function onElementRemoval (line 1900) | function onElementRemoval(target, callback, options = {}) {
  function createKeyPredicate (line 1940) | function createKeyPredicate(keyFilter) {
  function onKeyStroke (line 1949) | function onKeyStroke(...args) {
  function onKeyDown (line 1985) | function onKeyDown(key, handler, options = {}) {
  function onKeyPressed (line 1988) | function onKeyPressed(key, handler, options = {}) {
  function onKeyUp (line 1991) | function onKeyUp(key, handler, options = {}) {
  function onLongPress (line 1996) | function onLongPress(target, handler, options) {
  function isFocusedElementEditable (line 2079) | function isFocusedElementEditable() {
  function isTypedCharValid (line 2092) | function isTypedCharValid({
  function onStartTyping (line 2108) | function onStartTyping(callback, options = {}) {
  function templateRef (line 2118) | function templateRef(key, initialValue = null) {
  function useActiveElement (line 2138) | function useActiveElement(options = {}) {
  function useRafFn (line 2187) | function useRafFn(fn, options = {}) {
  function useAnimate (line 2236) | function useAnimate(target, keyframes, options) {
  function useAsyncQueue (line 2443) | function useAsyncQueue(tasks, options) {
  function whenAborted (line 2506) | function whenAborted(signal) {
  function useAsyncState (line 2515) | function useAsyncState(promise, initialState, options) {
  function getDefaultSerialization (line 2582) | function getDefaultSerialization(target) {
  function useBase64 (line 2594) | function useBase64(target, options) {
  function imgLoaded (line 2650) | function imgLoaded(img) {
  function blobToBase64 (line 2662) | function blobToBase64(blob) {
  function useBattery (line 2672) | function useBattery(options = {}) {
  function useBluetooth (line 2702) | function useBluetooth(options) {
  function useSSRWidth (line 2775) | function useSSRWidth() {
  function provideSSRWidth (line 2779) | function provideSSRWidth(width, app) {
  function useMediaQuery (line 2786) | function useMediaQuery(query, options = {}) {
  function useBreakpoints (line 2902) | function useBreakpoints(breakpoints, options = {}) {
  function useBroadcastChannel (line 2977) | function useBroadcastChannel(options) {
  function useBrowserLocation (line 3037) | function useBrowserLocation(options = {}) {
  function useCached (line 3071) | function useCached(refValue, comparator = (a, b) => a === b, watchOption...
  function usePermission (line 3079) | function usePermission(permissionDesc, options = {}) {
  function useClipboard (line 3119) | function useClipboard(options = {}) {
  function useClipboardItems (line 3192) | function useClipboardItems(options = {}) {
  function cloneFnJSON (line 3227) | function cloneFnJSON(source) {
  function useCloned (line 3230) | function useCloned(source, options = {}) {
  function getHandlers (line 3270) | function getHandlers() {
  function getSSRHandler (line 3275) | function getSSRHandler(key, fallback) {
  function setSSRHandler (line 3278) | function setSSRHandler(key, fn) {
  function usePreferredDark (line 3281) | function usePreferredDark(options) {
  function guessSerializerType (line 3284) | function guessSerializerType(rawInit) {
  function useStorage (line 3322) | function useStorage(key, defaults2, storage, options = {}) {
  function useColorMode (line 3450) | function useColorMode(options = {}) {
  function useConfirmDialog (line 3538) | function useConfirmDialog(revealed = ref(false)) {
  function useCountdown (line 3570) | function useCountdown(initialCountdown, options) {
  function useCssVar (line 3611) | function useCssVar(prop, target, options = {}) {
  function useCurrentElement (line 3657) | function useCurrentElement(rootComponent) {
  function useCycleList (line 3667) | function useCycleList(list, options) {
  function useDark (line 3713) | function useDark(options = {}) {
  function fnBypass (line 3747) | function fnBypass(v) {
  function fnSetSource (line 3750) | function fnSetSource(source, value) {
  function defaultDump (line 3753) | function defaultDump(clone) {
  function defaultParse (line 3756) | function defaultParse(clone) {
  function useManualRefHistory (line 3759) | function useManualRefHistory(source, options = {}) {
  function useRefHistory (line 3826) | function useRefHistory(source, options = {}) {
  function useDebouncedRefHistory (line 3887) | function useDebouncedRefHistory(source, options = {}) {
  function useDeviceMotion (line 3894) | function useDeviceMotion(options = {}) {
  function useDeviceOrientation (line 3974) | function useDeviceOrientation(options = {}) {
  function useDevicePixelRatio (line 3997) | function useDevicePixelRatio(options = {}) {
  function useDevicesList (line 4012) | function useDevicesList(options = {}) {
  function useDisplayMedia (line 4074) | function useDisplayMedia(options = {}) {
  function useDocumentVisibility (line 4127) | function useDocumentVisibility(options = {}) {
  function useDraggable (line 4137) | function useDraggable(target, options = {}) {
  function useDropZone (line 4244) | function useDropZone(target, options = {}) {
  function useResizeObserver (line 4331) | function useResizeObserver(target, callback, options = {}) {
  function useElementBounding (line 4369) | function useElementBounding(target, options = {}) {
  function useElementByPoint (line 4441) | function useElementByPoint(options) {
  function useElementHover (line 4467) | function useElementHover(el, options = {}) {
  function useElementSize (line 4499) | function useElementSize(target, initialSize = { width: 0, height: 0 }, o...
  function useIntersectionObserver (line 4555) | function useIntersectionObserver(target, callback, options = {}) {
  function useElementVisibility (line 4613) | function useElementVisibility(element, options = {}) {
  function useEventBus (line 4644) | function useEventBus(key) {
  function resolveNestedOptions$1 (line 4679) | function resolveNestedOptions$1(options) {
  function useEventSource (line 4684) | function useEventSource(url, events2 = [], options = {}) {
  function useEyeDropper (line 4773) | function useEyeDropper(options = {}) {
  function useFavicon (line 4787) | function useFavicon(newIcon = null, options = {}) {
  function isFetchOptions (line 4822) | function isFetchOptions(obj) {
  function isAbsoluteURL (line 4826) | function isAbsoluteURL(url) {
  function headersToObject (line 4829) | function headersToObject(headers) {
  function combineCallbacks (line 4834) | function combineCallbacks(combination, ...callbacks) {
  function createFetch (line 4852) | function createFetch(config = {}) {
  function useFetch (line 4897) | function useFetch(url, ...args) {
  function joinPaths (line 5148) | function joinPaths(start, end) {
  function prepareInitialFiles (line 5163) | function prepareInitialFiles(files) {
  function useFileDialog (line 5174) | function useFileDialog(options = {}) {
  function useFileSystemAccess (line 5226) | function useFileSystemAccess(options = {}) {
  function useFocus (line 5320) | function useFocus(target, options = {}) {
  function useFocusWithin (line 5353) | function useFocusWithin(target, options = {}) {
  function useFps (line 5370) | function useFps(options) {
  function useFullscreen (line 5397) | function useFullscreen(target, options = {}) {
  function mapGamepadToXbox360Controller (line 5508) | function mapGamepadToXbox360Controller(gamepad) {
  function useGamepad (line 5551) | function useGamepad(options = {}) {
  function useGeolocation (line 5618) | function useGeolocation(options = {}) {
  function useIdle (line 5677) | function useIdle(timeout = oneMinute, options = {}) {
  function loadImage (line 5719) | async function loadImage(options) {
  function useImage (line 5752) | function useImage(options, asyncStateOptions = {}) {
  function resolveElement (line 5768) | function resolveElement(el) {
  function useScroll (line 5776) | function useScroll(element, options = {}) {
  function useInfiniteScroll (line 5939) | function useInfiniteScroll(element, onLoadMore, options = {}) {
  function useKeyModifier (line 5994) | function useKeyModifier(modifier, options = {}) {
  function useLocalStorage (line 6011) | function useLocalStorage(key, initialValue, options = {}) {
  function useMagicKeys (line 6025) | function useMagicKeys(options = {}) {
  function usingElRef (line 6115) | function usingElRef(source, cb) {
  function timeRangeToArray (line 6119) | function timeRangeToArray(timeRanges) {
  function tracksToArray (line 6125) | function tracksToArray(tracks) {
  function useMediaControls (line 6132) | function useMediaControls(target, options = {}) {
  function useMemoize (line 6426) | function useMemoize(resolver, options) {
  function useMemory (line 6458) | function useMemory(options = {}) {
  function useMouse (line 6475) | function useMouse(options = {}) {
  function useMouseInElement (line 6547) | function useMouseInElement(target, options = {}) {
  function useMousePressed (line 6612) | function useMousePressed(options = {}) {
  function useNavigatorLanguage (line 6660) | function useNavigatorLanguage(options = {}) {
  function useNetwork (line 6674) | function useNetwork(options = {}) {
  function useNow (line 6730) | function useNow(options = {}) {
  function useObjectUrl (line 6747) | function useObjectUrl(object) {
  function useClamp (line 6766) | function useClamp(value, min, max) {
  function useOffsetPagination (line 6779) | function useOffsetPagination(options) {
  function useOnline (line 6832) | function useOnline(options = {}) {
  function usePageLeave (line 6836) | function usePageLeave(options = {}) {
  function useScreenOrientation (line 6854) | function useScreenOrientation(options = {}) {
  function useParallax (line 6885) | function useParallax(target, options = {}) {
  function useParentElement (line 6959) | function useParentElement(element = useCurrentElement()) {
  function usePerformanceObserver (line 6970) | function usePerformanceObserver(options, callback) {
  function usePointer (line 7010) | function usePointer(options = {}) {
  function usePointerLock (line 7033) | function usePointerLock(target, options = {}) {
  function usePointerSwipe (line 7085) | function usePointerSwipe(target, options = {}) {
  function usePreferredColorScheme (line 7179) | function usePreferredColorScheme(options) {
  function usePreferredContrast (line 7190) | function usePreferredContrast(options) {
  function usePreferredLanguages (line 7204) | function usePreferredLanguages(options = {}) {
  function usePreferredReducedMotion (line 7215) | function usePreferredReducedMotion(options) {
  function usePreferredReducedTransparency (line 7223) | function usePreferredReducedTransparency(options) {
  function usePrevious (line 7231) | function usePrevious(value, initialValue) {
  function useScreenSafeArea (line 7246) | function useScreenSafeArea() {
  function getValue (line 7277) | function getValue(position) {
  function useScriptTag (line 7280) | function useScriptTag(src, onLoaded = noop, options = {}) {
  function checkOverflowScroll (line 7361) | function checkOverflowScroll(ele) {
  function preventDefault (line 7372) | function preventDefault(rawEvent) {
  function useScrollLock (line 7384) | function useScrollLock(element, initialState = false) {
  function useSessionStorage (line 7443) | function useSessionStorage(key, initialValue, options = {}) {
  function useShare (line 7447) | function useShare(shareOptions = {}, options = {}) {
  function useSorted (line 7471) | function useSorted(...args) {
  function useSpeechRecognition (line 7502) | function useSpeechRecognition(options = {}) {
  function useSpeechSynthesis (line 7582) | function useSpeechSynthesis(text, options = {}) {
  function useStepper (line 7673) | function useStepper(steps, initialStep) {
  function useStorageAsync (line 7747) | function useStorageAsync(key, initialValue, storage, options = {}) {
  function useStyleTag (line 7825) | function useStyleTag(css, options = {}) {
  function useSwipe (line 7876) | function useSwipe(target, options = {}) {
  function useTemplateRefsList (line 7951) | function useTemplateRefsList() {
  function useTextDirection (line 7962) | function useTextDirection(options = {}) {
  function getRangesFromSelection (line 7998) | function getRangesFromSelection(selection) {
  function useTextSelection (line 8003) | function useTextSelection(options = {}) {
  function useTextareaAutosize (line 8028) | function useTextareaAutosize(options) {
  function useThrottledRefHistory (line 8068) | function useThrottledRefHistory(source, options = {}) {
  function DEFAULT_FORMATTER (line 8098) | function DEFAULT_FORMATTER(date) {
  function useTimeAgo (line 8101) | function useTimeAgo(time, options = {}) {
  function formatTimeAgo (line 8117) | function formatTimeAgo(from, options = {}, now2 = Date.now()) {
  function useTimeoutPoll (line 8163) | function useTimeoutPoll(fn, interval, options = {}) {
  function useTimestamp (line 8193) | function useTimestamp(options = {}) {
  function useTitle (line 8217) | function useTitle(newTitle = null, options = {}) {
  function createEasingFunction (line 8286) | function createEasingFunction([p0, p1, p2, p3]) {
  function lerp (line 8305) | function lerp(a, b, alpha) {
  function toVec (line 8308) | function toVec(t) {
  function executeTransition (line 8311) | function executeTransition(source, from, to, options = {}) {
  function useTransition (line 8350) | function useTransition(source, options = {}) {
  function useUrlSearchParams (line 8388) | function useUrlSearchParams(mode = "history", options = {}) {
  function useUserMedia (line 8489) | function useUserMedia(options = {}) {
  function useVModel (line 8573) | function useVModel(props, key, emit, options = {}) {
  function useVModels (line 8634) | function useVModels(props, emit, options = {}) {
  function useVibrate (line 8646) | function useVibrate(options) {
  function useVirtualList (line 8682) | function useVirtualList(list, options) {
  function useVirtualListResources (line 8697) | function useVirtualListResources(list) {
  function createGetViewCapacity (line 8705) | function createGetViewCapacity(state, source, itemSize) {
  function createGetOffset (line 8722) | function createGetOffset(source, itemSize) {
  function createCalculateRange (line 8739) | function createCalculateRange(type, overscan, getOffset, getViewCapacity...
  function createGetDistance (line 8758) | function createGetDistance(itemSize, source) {
  function useWatchForSizes (line 8768) | function useWatchForSizes(size, list, containerRef, calculateRange) {
  function createComputedTotalSize (line 8773) | function createComputedTotalSize(itemSize, source) {
  function createScrollTo (line 8784) | function createScrollTo(type, calculateRange, getDistance, containerRef) {
  function useHorizontalVirtualList (line 8792) | function useHorizontalVirtualList(options, list) {
  function useVerticalVirtualList (line 8824) | function useVerticalVirtualList(options, list) {
  function useWakeLock (line 8855) | function useWakeLock(options = {}) {
  function useWebNotification (line 8904) | function useWebNotification(options = {}) {
  function resolveNestedOptions (line 8984) | function resolveNestedOptions(options) {
  function useWebSocket (line 8989) | function useWebSocket(url, options = {}) {
  function useWebWorker (line 9145) | function useWebWorker(arg0, workerOptions, options) {
  function depsParser (line 9183) | function depsParser(deps, localDeps) {
  function jobRunner (line 9199) | function jobRunner(userFunc) {
  function createWorkerBlobUrl (line 9209) | function createWorkerBlobUrl(fn, deps, localDeps) {
  function useWebWorkerFn (line 9215) | function useWebWorkerFn(fn, options = {}) {
  function useWindowFocus (line 9298) | function useWindowFocus(options = {}) {
  function useWindowScroll (line 9312) | function useWindowScroll(options = {}) {
  function useWindowSize (line 9316) | function useWindowSize(options = {}) {

FILE: docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js
  method "../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.1_@types+node@22.10.5__jiti@2.4.2_postcss@8.4.49_tsx_s7k37zks4wtn7x2grzma6lrsfa/node_modules/tsup/assets/esm_shims.js" (line 31) | "../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.1_@ty...
  method "../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js" (line 36) | "../../node_modules/.pnpm/rfdc@1.4.1/node_modules/rfdc/index.js"(exports...
  function toUpper (line 236) | function toUpper(_, c) {
  function classify (line 239) | function classify(str) {
  function basename (line 242) | function basename(filename, ext) {
  function isUrlString (line 256) | function isUrlString(str) {
  function debounce (line 265) | function debounce(fn, wait = 25, options = {}) {
  function _applyPromised (line 314) | async function _applyPromised(fn, _this, args) {
  function flatHooks (line 319) | function flatHooks(configHooks, hooks2 = {}, parentName) {
  function serialTaskCaller (line 334) | function serialTaskCaller(hooks2, args) {
  function parallelTaskCaller (line 342) | function parallelTaskCaller(hooks2, args) {
  function callEachWith (line 347) | function callEachWith(callbacks, arg0) {
  method constructor (line 353) | constructor() {
  method hook (line 363) | hook(name, function_, options = {}) {
  method hookOnce (line 405) | hookOnce(name, function_) {
  method removeHook (line 418) | removeHook(name, function_) {
  method deprecateHook (line 429) | deprecateHook(name, deprecated) {
  method deprecateHooks (line 437) | deprecateHooks(deprecatedHooks) {
  method addHooks (line 443) | addHooks(configHooks) {
  method removeHooks (line 454) | removeHooks(configHooks) {
  method removeAllHooks (line 460) | removeAllHooks() {
  method callHook (line 465) | callHook(name, ...arguments_) {
  method callHookParallel (line 469) | callHookParallel(name, ...arguments_) {
  method callHookWith (line 473) | callHookWith(caller, name, ...arguments_) {
  method beforeEach (line 494) | beforeEach(function_) {
  method afterEach (line 506) | afterEach(function_) {
  function createHooks (line 519) | function createHooks() {
  method "../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.1_@types+node@22.10.5__jiti@2.4.2_postcss@8.4.49_tsx_s7k37zks4wtn7x2grzma6lrsfa/node_modules/tsup/assets/esm_shims.js" (line 557) | "../../node_modules/.pnpm/tsup@8.3.5_@microsoft+api-extractor@7.48.1_@ty...
  method "../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/lib/speakingurl.js" (line 562) | "../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/li...
  method "../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/index.js" (line 2089) | "../../node_modules/.pnpm/speakingurl@14.0.1/node_modules/speakingurl/in...
  function getComponentTypeName (line 2103) | function getComponentTypeName(options) {
  function getComponentFileName (line 2111) | function getComponentFileName(options) {
  function saveComponentGussedName (line 2116) | function saveComponentGussedName(instance, name) {
  function getAppRecord (line 2120) | function getAppRecord(instance) {
  function isFragment (line 2126) | function isFragment(instance) {
  function getInstanceName (line 2135) | function getInstanceName(instance) {
  function getUniqueComponentId (line 2155) | function getUniqueComponentId(instance) {
  function getComponentInstance (line 2161) | function getComponentInstance(appRecord, instanceId) {
  function createRect (line 2166) | function createRect() {
  function getTextRect (line 2182) | function getTextRect(node) {
  function getFragmentRect (line 2188) | function getFragmentRect(vnode) {
  function mergeRects (line 2209) | function mergeRects(a, b) {
  function getComponentBoundingRect (line 2228) | function getComponentBoundingRect(instance) {
  function getRootElementsFromComponentInstance (line 2243) | function getRootElementsFromComponentInstance(instance) {
  function getFragmentRootElements (line 2250) | function getFragmentRootElements(vnode) {
  function getContainerElement (line 2297) | function getContainerElement() {
  function getCardElement (line 2300) | function getCardElement() {
  function getIndicatorElement (line 2303) | function getIndicatorElement() {
  function getNameElement (line 2306) | function getNameElement() {
  function getStyles (line 2309) | function getStyles(bounds) {
  function create (line 2317) | function create(options) {
  function update (line 2345) | function update(options) {
  function highlight (line 2362) | function highlight(instance) {
  function unhighlight (line 2370) | function unhighlight() {
  function inspectFn (line 2376) | function inspectFn(e) {
  function selectComponentFn (line 2392) | function selectComponentFn(e, cb) {
  function cancelInspectComponentHighLighter (line 2401) | function cancelInspectComponentHighLighter() {
  function inspectComponentHighLighter (line 2407) | function inspectComponentHighLighter() {
  function scrollToComponent (line 2427) | function scrollToComponent(options) {
  function waitForInspectorInit (line 2469) | function waitForInspectorInit(cb) {
  function setupInspector (line 2482) | function setupInspector() {
  function getComponentInspector (line 2490) | function getComponentInspector() {
  function isReadonly (line 2507) | function isReadonly(value) {
  function isReactive (line 2513) | function isReactive(value) {
  function isRef (line 2525) | function isRef(r) {
  function toRaw (line 2528) | function toRaw(observed) {
  method constructor (line 2537) | constructor() {
  method set (line 2540) | set(object, path, value, cb) {
  method get (line 2566) | get(object, path) {
  method has (line 2580) | has(object, path, parent = false) {
  method createDefaultSetCallback (line 2593) | createDefaultSetCallback(state) {
  method set (line 2619) | set(ref, value) {
  method get (line 2646) | get(ref) {
  method isRef (line 2649) | isRef(ref) {
  function getTimelineLayersStateFromStorage (line 2658) | function getTimelineLayersStateFromStorage() {
  method get (line 2686) | get(target22, prop, receiver) {
  function addTimelineLayer (line 2690) | function addTimelineLayer(options, descriptor) {
  method get (line 2702) | get(target22, prop, receiver) {
  function addInspector (line 2709) | function addInspector(inspector, descriptor) {
  function getActiveInspectors (line 2722) | function getActiveInspectors() {
  function getInspector (line 2738) | function getInspector(id, app) {
  function createDevToolsCtxHooks (line 2778) | function createDevToolsCtxHooks() {
  function initStateFactory (line 2920) | function initStateFactory() {
  method get (line 2945) | get(_target, prop, receiver) {
  method get (line 2952) | get(_target, prop, receiver) {
  function updateAllStates (line 2960) | function updateAllStates() {
  function setActiveAppRecord (line 2969) | function setActiveAppRecord(app) {
  function setActiveAppRecordId (line 2973) | function setActiveAppRecordId(id) {
  method get (line 2978) | get(target22, property) {
  method deleteProperty (line 2990) | deleteProperty(target22, property) {
  method set (line 2994) | set(target22, property, value) {
  function onDevToolsConnected (line 3001) | function onDevToolsConnected(fn) {
  function addCustomTab (line 3025) | function addCustomTab(tab) {
  function addCustomCommand (line 3035) | function addCustomCommand(action) {
  function removeCustomCommand (line 3049) | function removeCustomCommand(actionId) {
  function openInEditor (line 3057) | function openInEditor(options = {}) {
  method get (line 3085) | get(target22, prop, receiver) {
  function _getSettings (line 3089) | function _getSettings(settings) {
  function getPluginLocalKey (line 3096) | function getPluginLocalKey(pluginId) {
  function getPluginSettingsOptions (line 3099) | function getPluginSettingsOptions(pluginId) {
  function getPluginSettings (line 3107) | function getPluginSettings(pluginId, fallbackValue) {
  function initPluginSettings (line 3122) | function initPluginSettings(pluginId, settings) {
  function setPluginSettings (line 3129) | function setPluginSettings(pluginId, key, value) {
  method vueAppInit (line 3167) | vueAppInit(fn) {
  method vueAppUnmount (line 3170) | vueAppUnmount(fn) {
  method vueAppConnected (line 3173) | vueAppConnected(fn) {
  method componentAdded (line 3176) | componentAdded(fn) {
  method componentEmit (line 3179) | componentEmit(fn) {
  method componentUpdated (line 3182) | componentUpdated(fn) {
  method componentRemoved (line 3185) | componentRemoved(fn) {
  method setupDevtoolsPlugin (line 3188) | setupDevtoolsPlugin(fn) {
  method perfStart (line 3191) | perfStart(fn) {
  method perfEnd (line 3194) | perfEnd(fn) {
  method setupDevToolsPlugin (line 3200) | setupDevToolsPlugin(pluginDescriptor, setupFn) {
  method constructor (line 3205) | constructor({ plugin, ctx }) {
  method on (line 3209) | get on() {
  method notifyComponentUpdate (line 3245) | notifyComponentUpdate(instance) {
  method addInspector (line 3270) | addInspector(options) {
  method sendInspectorTree (line 3276) | sendInspectorTree(inspectorId) {
  method sendInspectorState (line 3282) | sendInspectorState(inspectorId) {
  method selectInspectorNode (line 3288) | selectInspectorNode(inspectorId, nodeId) {
  method visitComponentTree (line 3291) | visitComponentTree(payload) {
  method now (line 3295) | now() {
  method addTimelineLayer (line 3301) | addTimelineLayer(options) {
  method addTimelineEvent (line 3304) | addTimelineEvent(options) {
  method getSettings (line 3311) | getSettings(pluginId) {
  method getComponentInstances (line 3315) | getComponentInstances(app) {
  method getComponentBounds (line 3318) | getComponentBounds(instance) {
  method getComponentName (line 3321) | getComponentName(instance) {
  method highlightElement (line 3324) | highlightElement(instance) {
  method unhighlightElement (line 3328) | unhighlightElement() {
  function setupDevToolsPlugin (line 3364) | function setupDevToolsPlugin(pluginDescriptor, setupFn) {
  function callDevToolsPluginSetupFn (line 3367) | function callDevToolsPluginSetupFn(plugin, app) {
  function registerDevToolsPlugin (line 3385) | function registerDevToolsPlugin(app, options) {
  method get (line 3411) | get(target22, property) {
  method get (line 3416) | get(target22, property) {
  function getRoutes (line 3422) | function getRoutes(router) {
  function filterRoutes (line 3426) | function filterRoutes(routes) {
  function filterCurrentRoute (line 3439) | function filterCurrentRoute(route) {
  function normalizeRouterInfo (line 3455) | function normalizeRouterInfo(appRecord, activeAppRecord2) {
  function createDevToolsApi (line 3482) | function createDevToolsApi(hooks2) {
  method state (line 3616) | get state() {
  function onDevToolsClientConnected (line 3635) | function onDevToolsClientConnected(fn) {
  function toggleHighPerfMode (line 3651) | function toggleHighPerfMode(state) {
  function updateDevToolsClientDetected (line 3660) | function updateDevToolsClientDetected(params) {
  method constructor (line 3679) | constructor() {
  method set (line 3683) | set(key, value) {
  method getByKey (line 3687) | getByKey(key) {
  method getByValue (line 3690) | getByValue(value) {
  method clear (line 3693) | clear() {
  method constructor (line 3699) | constructor(generateIdentifier) {
  method register (line 3703) | register(value, identifier) {
  method clear (line 3712) | clear() {
  method getIdentifier (line 3715) | getIdentifier(value) {
  method getValue (line 3718) | getValue(identifier) {
  method constructor (line 3723) | constructor() {
  method register (line 3727) | register(value, options) {
  method getAllowedProps (line 3737) | getAllowedProps(value) {
  function valuesOfObj (line 3743) | function valuesOfObj(record) {
  function find (line 3755) | function find(record, predicate) {
  function forEach (line 3769) | function forEach(record, run) {
  function includes (line 3772) | function includes(arr, value) {
  function findArr (line 3775) | function findArr(record, predicate) {
  method constructor (line 3785) | constructor() {
  method register (line 3788) | register(transformer) {
  method findApplicable (line 3791) | findApplicable(v) {
  method findByName (line 3794) | findByName(name) {
  function simpleTransformation (line 3856) | function simpleTransformation(isApplicable, annotation, transform, untra...
  function compositeTransformation (line 3921) | function compositeTransformation(isApplicable, annotation, transform, un...
  function isInstanceOfRegisteredClass (line 3966) | function isInstanceOfRegisteredClass(potentialClass, superJson) {
  function validatePath (line 4063) | function validatePath(path) {
  function traverse (line 4167) | function traverse(tree, walker2, origin = []) {
  function applyValueAnnotations (line 4183) | function applyValueAnnotations(plain, annotations, superJson) {
  function applyReferentialEqualityAnnotations (line 4189) | function applyReferentialEqualityAnnotations(plain, annotations) {
  function addIdentity (line 4210) | function addIdentity(object, path, identities) {
  function generateReferentialEqualityAnnotations (line 4218) | function generateReferentialEqualityAnnotations(identitites, dedupe) {
  function getType2 (line 4307) | function getType2(payload) {
  function isArray2 (line 4310) | function isArray2(payload) {
  function isPlainObject3 (line 4313) | function isPlainObject3(payload) {
  function isNull2 (line 4319) | function isNull2(payload) {
  function isOneOf (line 4322) | function isOneOf(a, b, c, d, e) {
  function isUndefined2 (line 4325) | function isUndefined2(payload) {
  function assignProp (line 4329) | function assignProp(carry, key, newVal, originalObject, includeNonenumer...
  function copy (line 4342) | function copy(target22, options = {}) {
  method constructor (line 4365) | constructor({ dedupe = false } = {}) {
  method serialize (line 4375) | serialize(object) {
  method deserialize (line 4396) | deserialize(payload) {
  method stringify (line 4407) | stringify(object) {
  method parse (line 4410) | parse(string) {
  method registerClass (line 4413) | registerClass(v, options) {
  method registerSymbol (line 4416) | registerSymbol(v, identifier) {
  method registerCustom (line 4419) | registerCustom(transformer, name) {
  method allowErrorProps (line 4425) | allowErrorProps(...props) {

FILE: docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js
  function _arrayLikeToArray (line 346) | function _arrayLikeToArray(r, a) {
  function _arrayWithoutHoles (line 351) | function _arrayWithoutHoles(r) {
  function _defineProperty (line 354) | function _defineProperty(e, r, t) {
  function _iterableToArray (line 362) | function _iterableToArray(r) {
  function _nonIterableSpread (line 365) | function _nonIterableSpread() {
  function ownKeys (line 368) | function ownKeys(e, r) {
  function _objectSpread2 (line 378) | function _objectSpread2(e) {
  function _toConsumableArray (line 389) | function _toConsumableArray(r) {
  function _toPrimitive (line 392) | function _toPrimitive(t, r) {
  function _toPropertyKey (line 402) | function _toPropertyKey(t) {
  function _unsupportedIterableToArray (line 406) | function _unsupportedIterableToArray(r, a) {
  method active (line 932) | get active() {
  method paused (line 935) | get paused() {
  function useFocusTrap (line 1069) | function useFocusTrap(target, options = {}) {

FILE: docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js
  method constructor (line 16) | constructor(ctx, iframes = true, exclude = [], iframesTimeout = 5e3) {
  method matches (line 30) | static matches(element, selector) {
  method getContexts (line 51) | getContexts() {
  method getIframeContents (line 88) | getIframeContents(ifr, successFn, errorFn = () => {
  method isIframeBlank (line 110) | isIframeBlank(ifr) {
  method observeIframeLoad (line 124) | observeIframeLoad(ifr, successFn, errorFn) {
  method onIframeReady (line 163) | onIframeReady(ifr, successFn, errorFn) {
  method waitForIframes (line 188) | waitForIframes(ctx, done) {
  method forEachIframe (line 230) | forEachIframe(ctx, filter, each, end = () => {
  method createIterator (line 265) | createIterator(ctx, whatToShow, filter) {
  method createInstanceOnIframe (line 274) | createInstanceOnIframe(contents) {
  method compareNodeIframe (line 287) | compareNodeIframe(node, prevNode, ifr) {
  method getIteratorNode (line 314) | getIteratorNode(itr) {
  method checkIframeFilter (line 353) | checkIframeFilter(node, prevNode, currIfr, ifr) {
  method handleOpenIframes (line 389) | handleOpenIframes(ifr, whatToShow, eCb, fCb) {
  method iterateThroughNodes (line 412) | iterateThroughNodes(whatToShow, ctx, eachCb, filterCb, doneCb) {
  method forEachNode (line 461) | forEachNode(whatToShow, each, filter, done = () => {
  method constructor (line 504) | constructor(ctx) {
  method opt (line 520) | set opt(val) {
  method opt (line 548) | get opt() {
  method iterator (line 556) | get iterator() {
  method log (line 571) | log(msg, level = "debug") {
  method escapeStr (line 586) | escapeStr(str) {
  method createRegExp (line 596) | createRegExp(str) {
  method createSynonymsRegExp (line 626) | createSynonymsRegExp(str) {
  method processSynomyms (line 649) | processSynomyms(str) {
  method setupWildcardsRegExp (line 662) | setupWildcardsRegExp(str) {
  method createWildcardsRegExp (line 677) | createWildcardsRegExp(str) {
  method setupIgnoreJoinersRegExp (line 688) | setupIgnoreJoinersRegExp(str) {
  method createJoinersRegExp (line 707) | createJoinersRegExp(str) {
  method createDiacriticsRegExp (line 724) | createDiacriticsRegExp(str) {
  method createMergedBlanksRegExp (line 796) | createMergedBlanksRegExp(str) {
  method createAccuracyRegExp (line 809) | createAccuracyRegExp(str) {
  method getSeparatedKeywords (line 839) | getSeparatedKeywords(sv) {
  method isNumeric (line 869) | isNumeric(value) {
  method checkRanges (line 893) | checkRanges(array) {
  method callNoMatchOnInvalidRanges (line 932) | callNoMatchOnInvalidRanges(range, last) {
  method checkWhitespaceRanges (line 966) | checkWhitespaceRanges(range, originalLength, string) {
  method getTextNodes (line 1012) | getTextNodes(cb) {
  method matchesExclude (line 1041) | matchesExclude(el) {
  method wrapRangeInTextNode (line 1061) | wrapRangeInTextNode(node, start, end) {
  method wrapRangeInMappedTextNode (line 1106) | wrapRangeInMappedTextNode(dict, start, end, filterCb, eachCb) {
  method wrapMatches (line 1161) | wrapMatches(regex, ignoreGroups, filterCb, eachCb, endCb) {
  method wrapMatchesAcrossElements (line 1215) | wrapMatchesAcrossElements(regex, ignoreGroups, filterCb, eachCb, endCb) {
  method wrapRangeFromIndex (line 1265) | wrapRangeFromIndex(ranges, filterCb, eachCb, endCb) {
  method unwrapMatches (line 1297) | unwrapMatches(node) {
  method normalizeTextNode (line 1318) | normalizeTextNode(node) {
  method markRegExp (line 1382) | markRegExp(regexp, opt) {
  method mark (line 1518) | mark(sv, opt) {
  method markRanges (line 1590) | markRanges(rawRanges, opt) {
  method unmark (line 1620) | unmark(opt) {
  function Mark2 (line 1642) | function Mark2(ctx) {

FILE: docs/.vitepress/cache/deps/vitepress___minisearch.js
  function __awaiter (line 2) | function __awaiter(thisArg, _arguments, P, generator) {
  method constructor (line 34) | constructor(set, type) {
  method next (line 41) | next() {
  method dive (line 46) | dive() {
  method backtrack (line 58) | backtrack() {
  method key (line 70) | key() {
  method value (line 73) | value() {
  method result (line 76) | result() {
  method [Symbol.iterator] (line 86) | [Symbol.iterator]() {
  method constructor (line 151) | constructor(tree = /* @__PURE__ */ new Map(), prefix = "") {
  method atPrefix (line 185) | atPrefix(prefix) {
  method clear (line 205) | clear() {
  method delete (line 213) | delete(key) {
  method entries (line 221) | entries() {
  method forEach (line 228) | forEach(fn) {
  method fuzzyGet (line 261) | fuzzyGet(key, maxEditDistance) {
  method get (line 270) | get(key) {
  method has (line 279) | has(key) {
  method keys (line 287) | keys() {
  method set (line 296) | set(key, value) {
  method size (line 308) | get size() {
  method update (line 338) | update(key, fn) {
  method fetch (line 363) | fetch(key, initial) {
  method values (line 379) | values() {
  method [Symbol.iterator] (line 385) | [Symbol.iterator]() {
  method from (line 394) | static from(entries) {
  method fromObject (line 407) | static fromObject(object) {
  method constructor (line 567) | constructor(options) {
  method add (line 593) | add(document) {
  method addAll (line 629) | addAll(documents) {
  method addAllAsync (line 644) | addAllAsync(documents, options = {}) {
  method remove (line 674) | remove(document) {
  method removeAll (line 718) | removeAll(documents) {
  method discard (line 779) | discard(id) {
  method maybeAutoVacuum (line 795) | maybeAutoVacuum() {
  method discardAll (line 814) | discardAll(ids) {
  method replace (line 841) | replace(updatedDocument) {
  method vacuum (line 886) | vacuum(options = {}) {
  method conditionalVacuum (line 889) | conditionalVacuum(options, conditions) {
  method performVacuuming (line 908) | performVacuuming(options, conditions) {
  method vacuumConditionsMet (line 943) | vacuumConditionsMet(conditions) {
  method isVacuuming (line 955) | get isVacuuming() {
  method dirtCount (line 961) | get dirtCount() {
  method dirtFactor (line 971) | get dirtFactor() {
  method has (line 980) | has(id) {
  method getStoredFields (line 990) | getStoredFields(id) {
  method search (line 1157) | search(query, searchOptions = {}) {
  method autoSuggest (line 1243) | autoSuggest(queryString, options = {}) {
  method documentCount (line 1266) | get documentCount() {
  method termCount (line 1272) | get termCount() {
  method loadJSON (line 1296) | static loadJSON(json, options) {
  method loadJSONAsync (line 1315) | static loadJSONAsync(json, options) {
  method getDefault (line 1344) | static getDefault(optionName) {
  method loadJS (line 1354) | static loadJS(js, options) {
  method loadJSAsync (line 1379) | static loadJSAsync(js, options) {
  method instantiateMiniSearch (line 1409) | static instantiateMiniSearch(js, options) {
  method executeQuery (line 1427) | executeQuery(query, searchOptions = {}) {
  method executeQuerySpec (line 1447) | executeQuerySpec(query, searchOptions) {
  method executeWildcardQuery (line 1491) | executeWildcardQuery(searchOptions) {
  method combineResults (line 1507) | combineResults(results, combineWith = OR) {
  method toJSON (line 1542) | toJSON() {
  method termResults (line 1567) | termResults(sourceTerm, derivedTerm, termWeight, termBoost, fieldTermDat...
  method addTerm (line 1615) | addTerm(fieldId, documentId, term) {
  method removeTerm (line 1630) | removeTerm(fieldId, documentId, term) {
  method warnDocumentChanged (line 1655) | warnDocumentChanged(shortDocumentId, fieldId, term) {
  method addDocumentId (line 1666) | addDocumentId(documentId) {
  method addFields (line 1677) | addFields(fields) {
  method addFieldLength (line 1685) | addFieldLength(documentId, fieldId, count, length) {
  method removeFieldLength (line 1697) | removeFieldLength(documentId, fieldId, count, length) {
  method saveStoredFields (line 1708) | saveStoredFields(documentId, doc) {

FILE: main.py
  function lifespan (line 30) | async def lifespan(app: FastAPI):
  function load_config (line 61) | async def load_config():
  function migrate_password_to_hash (line 76) | async def migrate_password_to_hash():
  function refresh_settings_middleware (line 90) | async def refresh_settings_middleware(request, call_next):
  function index (line 118) | async def index(request=None, exc=None):
  function robots (line 136) | async def robots():
  function get_config (line 141) | async def get_config():
Condensed preview — 85 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,360K chars).
[
  {
    "path": ".dockerignore",
    "chars": 442,
    "preview": "# Git\n.git\n.gitignore\n.gitattributes\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n\n# Documentation\n*.md\ndocs/\nLICENSE\n\n# Development"
  },
  {
    "path": ".gitattributes",
    "chars": 93,
    "preview": "*.js linguist-language=python\n*.css linguist-language=python\n*.html linguist-language=python\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 834,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.md",
    "chars": 126,
    "preview": "---\nname: Custom issue template\nabout: Describe this issue template's purpose here.\ntitle: ''\nlabels: ''\nassignees: ''\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 595,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
  },
  {
    "path": ".github/workflows/docker-image.yml",
    "chars": 1708,
    "preview": "name: Build and push Docker image\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - master\n      - dev\n    tags:\n "
  },
  {
    "path": ".github/workflows/docs.yml",
    "chars": 1634,
    "preview": "# 构建 VitePress 站点并将其部署到 GitHub Pages 的示例工作流程\n#\nname: Deploy VitePress site to Pages\n\non:\n  # 在针对 `main` 分支的推送上运行。如果你\n  #"
  },
  {
    "path": ".gitignore",
    "chars": 2439,
    "preview": "media/\nlogs/\n.idea\n/data\n__pycache__/\n*.py[cod]\n*$py.class\n# Created by .ignore support plugin (hsz.mobi)\n### Python tem"
  },
  {
    "path": "Dockerfile",
    "chars": 1275,
    "preview": "# 第一阶段:构建前端主题\nFROM node:20-alpine AS frontend-builder\n\nRUN apk add --no-cache git python3 make g++\n\nWORKDIR /build\n\n# 克隆"
  },
  {
    "path": "LICENSE",
    "chars": 7650,
    "preview": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007"
  },
  {
    "path": "SECURITY.md",
    "chars": 619,
    "preview": "# Security Policy\n\n## Supported Versions\n\nUse this section to tell people about which versions of your project are\ncurre"
  },
  {
    "path": "apps/__init__.py",
    "chars": 95,
    "preview": "# @Time    : 2023/8/13 20:43\n# @Author  : Lan\n# @File    : __init__.py.py\n# @Software: PyCharm\n"
  },
  {
    "path": "apps/admin/__init__.py",
    "chars": 95,
    "preview": "# @Time    : 2023/8/14 14:38\n# @Author  : Lan\n# @File    : __init__.py.py\n# @Software: PyCharm\n"
  },
  {
    "path": "apps/admin/dependencies.py",
    "chars": 3605,
    "preview": "# @Time    : 2023/8/15 17:43\n# @Author  : Lan\n# @File    : depends.py\n# @Software: PyCharm\nfrom fastapi import Header, H"
  },
  {
    "path": "apps/admin/schemas.py",
    "chars": 566,
    "preview": "import datetime\nfrom typing import Optional, Union\n\nfrom pydantic import BaseModel\n\n\nclass IDData(BaseModel):\n    id: in"
  },
  {
    "path": "apps/admin/services.py",
    "chars": 5103,
    "preview": "import os\nimport time\n\nfrom core.response import APIResponse\nfrom core.storage import FileStorageInterface, storages\nfro"
  },
  {
    "path": "apps/admin/views.py",
    "chars": 5323,
    "preview": "# @Time    : 2023/8/14 14:38\n# @Author  : Lan\n# @File    : views.py\n# @Software: PyCharm\nimport datetime\n\nfrom fastapi i"
  },
  {
    "path": "apps/base/__init__.py",
    "chars": 95,
    "preview": "# @Time    : 2023/8/13 20:43\n# @Author  : Lan\n# @File    : __init__.py.py\n# @Software: PyCharm\n"
  },
  {
    "path": "apps/base/dependencies.py",
    "chars": 1488,
    "preview": "from typing import Dict, Union\nfrom datetime import datetime, timedelta\nfrom fastapi import HTTPException, Request\n\n\ncla"
  },
  {
    "path": "apps/base/migrations/migrations_001.py",
    "chars": 1834,
    "preview": "from tortoise import connections\n\n\nasync def create_file_codes_table():\n    conn = connections.get(\"default\")\n    await "
  },
  {
    "path": "apps/base/migrations/migrations_002.py",
    "chars": 974,
    "preview": "from tortoise import connections\n\n\nasync def create_upload_chunk_and_update_file_codes_table():\n    conn = connections.g"
  },
  {
    "path": "apps/base/migrations/migrations_003.py",
    "chars": 929,
    "preview": "from tortoise import connections\n\n\nasync def create_presign_upload_session_table():\n    conn = connections.get(\"default\""
  },
  {
    "path": "apps/base/migrations/migrations_004.py",
    "chars": 312,
    "preview": "from tortoise import connections\n\n\nasync def add_save_path_to_uploadchunk():\n    conn = connections.get(\"default\")\n    a"
  },
  {
    "path": "apps/base/migrations/migrations_005.py",
    "chars": 2584,
    "preview": "from tortoise import connections\n\n\ndef _need_upgrade(columns: list[tuple]) -> bool:\n    for column in columns:\n        #"
  },
  {
    "path": "apps/base/models.py",
    "chars": 3419,
    "preview": "# @Time    : 2023/8/13 20:43\n# @Author  : Lan\n# @File    : models.py\n# @Software: PyCharm\nfrom typing import Optional\n\nf"
  },
  {
    "path": "apps/base/schemas.py",
    "chars": 696,
    "preview": "from pydantic import BaseModel\nfrom typing import Optional\n\n\nclass SelectFileModel(BaseModel):\n    code: str\n\n\nclass Ini"
  },
  {
    "path": "apps/base/utils.py",
    "chars": 3801,
    "preview": "import datetime\nimport hashlib\nimport os\nimport uuid\nfrom urllib.parse import unquote\n\nfrom fastapi import UploadFile, H"
  },
  {
    "path": "apps/base/views.py",
    "chars": 23002,
    "preview": "import datetime\nimport hashlib\nimport os\nimport uuid\nfrom datetime import timedelta\nfrom urllib.parse import unquote\n\nfr"
  },
  {
    "path": "core/__init__.py",
    "chars": 95,
    "preview": "# @Time    : 2023/8/11 20:06\n# @Author  : Lan\n# @File    : __init__.py.py\n# @Software: PyCharm\n"
  },
  {
    "path": "core/config.py",
    "chars": 750,
    "preview": "from apps.base.models import KeyValue\nfrom apps.base.utils import ip_limit\nfrom core.settings import DEFAULT_CONFIG, set"
  },
  {
    "path": "core/database.py",
    "chars": 4322,
    "preview": "import asyncio\nimport glob\nimport importlib\nimport os\nfrom contextlib import asynccontextmanager\nfrom typing import IO\n\n"
  },
  {
    "path": "core/logger.py",
    "chars": 582,
    "preview": "import logging\nimport sys\n\n\ndef setup_logger():\n    # 创建logger对象\n    _logger = logging.getLogger('FileCodeBox')\n    _log"
  },
  {
    "path": "core/response.py",
    "chars": 307,
    "preview": "# @Time    : 2023/8/14 11:48\n# @Author  : Lan\n# @File    : response.py\n# @Software: PyCharm\nfrom typing import Generic, "
  },
  {
    "path": "core/settings.py",
    "chars": 2981,
    "preview": "# @Time    : 2023/8/15 09:51\n# @Author  : Lan\n# @File    : settings.py\n# @Software: PyCharm\nfrom pathlib import Path\n\nBA"
  },
  {
    "path": "core/storage.py",
    "chars": 52903,
    "preview": "# @Time    : 2023/8/11 20:06\n# @Author  : Lan\n# @File    : storage.py\n# @Software: PyCharm\nimport base64\nimport hashlib\n"
  },
  {
    "path": "core/tasks.py",
    "chars": 3238,
    "preview": "# @Time    : 2023/8/15 22:00\n# @Author  : Lan\n# @File    : tasks.py\n# @Software: PyCharm\nimport asyncio\nimport datetime\n"
  },
  {
    "path": "core/utils.py",
    "chars": 3771,
    "preview": "# @Time    : 2023/8/13 19:54\n# @Author  : Lan\n# @File    : utils.py\n# @Software: PyCharm\nimport datetime\nimport hashlib\n"
  },
  {
    "path": "docker-compose.yml",
    "chars": 220,
    "preview": "version: \"3\"\nservices:\n  file-code-box:\n    image: lanol/filecodebox:latest\n    volumes:\n      - fcb-data:/app/data:rw\n "
  },
  {
    "path": "docs/.vitepress/cache/deps/_metadata.json",
    "chars": 1826,
    "preview": "{\n  \"hash\": \"8f855eaf\",\n  \"configHash\": \"1b3ca22f\",\n  \"lockfileHash\": \"bd28b2c2\",\n  \"browserHash\": \"29e84937\",\n  \"optimi"
  },
  {
    "path": "docs/.vitepress/cache/deps/chunk-CQOUZRMK.js",
    "chars": 370202,
    "preview": "// node_modules/.pnpm/@vue+shared@3.5.13/node_modules/@vue/shared/dist/shared.esm-bundler.js\nfunction makeMap(str) {\n  c"
  },
  {
    "path": "docs/.vitepress/cache/deps/chunk-KT7LHMJ2.js",
    "chars": 276616,
    "preview": "import {\n  Fragment,\n  TransitionGroup,\n  computed,\n  customRef,\n  defineComponent,\n  effectScope,\n  getCurrentInstance,"
  },
  {
    "path": "docs/.vitepress/cache/deps/package.json",
    "chars": 23,
    "preview": "{\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js",
    "chars": 132110,
    "preview": "// node_modules/.pnpm/@vue+devtools-shared@7.7.1/node_modules/@vue/devtools-shared/dist/index.js\nvar __create = Object.c"
  },
  {
    "path": "docs/.vitepress/cache/deps/vitepress___@vueuse_core.js",
    "chars": 10313,
    "preview": "import {\n  DefaultMagicKeysAliasMap,\n  StorageSerializers,\n  TransitionPresets,\n  assert,\n  breakpointsAntDesign,\n  brea"
  },
  {
    "path": "docs/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js",
    "chars": 45426,
    "preview": "import {\n  notNullish,\n  toArray,\n  tryOnScopeDispose,\n  unrefElement\n} from \"./chunk-KT7LHMJ2.js\";\nimport {\n  computed,"
  },
  {
    "path": "docs/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js",
    "chars": 56008,
    "preview": "// node_modules/.pnpm/mark.js@8.11.1/node_modules/mark.js/src/lib/domiterator.js\nvar DOMIterator = class _DOMIterator {\n"
  },
  {
    "path": "docs/.vitepress/cache/deps/vitepress___minisearch.js",
    "chars": 65438,
    "preview": "// node_modules/.pnpm/minisearch@7.1.1/node_modules/minisearch/dist/es/index.js\nfunction __awaiter(thisArg, _arguments, "
  },
  {
    "path": "docs/.vitepress/cache/deps/vue.js",
    "chars": 5563,
    "preview": "import {\n  BaseTransition,\n  BaseTransitionPropsValidators,\n  Comment,\n  DeprecationTypes,\n  EffectScope,\n  ErrorCodes,\n"
  },
  {
    "path": "docs/.vitepress/config.mts",
    "chars": 7001,
    "preview": "import { defineConfig } from 'vitepress'\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n "
  },
  {
    "path": "docs/.vitepress/theme/custom.css",
    "chars": 15041,
    "preview": "\n        :root {\n            /* --- 核心色板:清晨薄雾 (Teal -> Sky) --- */\n            --vp-c-brand-1: #14b8a6; /* Teal 500 */\n "
  },
  {
    "path": "docs/.vitepress/theme/index.ts",
    "chars": 104,
    "preview": "import DefaultTheme from 'vitepress/theme'\nimport './custom.css'\n\nexport default {\n  ...DefaultTheme,\n}\n"
  },
  {
    "path": "docs/api/index.md",
    "chars": 4652,
    "preview": "# FileCodeBox API 文档\n\n## API 版本: 2.1.0\n\n## 目录\n- [认证](#认证)\n- [分享接口](#分享接口)\n- [管理接口](#管理接口)\n\n## 认证\n\n部分接口需要在请求头中携带 `Authori"
  },
  {
    "path": "docs/api/presign-upload.md",
    "chars": 8722,
    "preview": "# 预签名上传 API 文档\n\n## 概述\n\n预签名上传功能提供统一的文件上传接口,根据后端存储类型自动选择最优上传方式:\n\n- **S3 存储**: 返回预签名 URL,客户端直传 S3(减少服务器带宽压力)\n- **其他存储**: 返回"
  },
  {
    "path": "docs/changelog.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/contributing.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/en/api/index.md",
    "chars": 6109,
    "preview": "# FileCodeBox API Documentation\n\n## API Version: 2.1.0\n\n## Table of Contents\n- [Authentication](#authentication)\n- [Shar"
  },
  {
    "path": "docs/en/changelog.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/en/contributing.md",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/en/guide/configuration.md",
    "chars": 6639,
    "preview": "# Configuration Guide\n\nFileCodeBox provides rich configuration options that can be customized through the admin panel or"
  },
  {
    "path": "docs/en/guide/getting-started.md",
    "chars": 1805,
    "preview": "# Getting Started\n\n## Introduction\n\nFileCodeBox is a simple and efficient file sharing tool that supports temporary file"
  },
  {
    "path": "docs/en/guide/introduction.md",
    "chars": 6281,
    "preview": "<div align=\"center\">\n\n<img src=\"https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/static/banners/img_1.png\" alt=\"Fi"
  },
  {
    "path": "docs/en/guide/management.md",
    "chars": 11549,
    "preview": "# Admin Panel\n\nFileCodeBox provides a fully-featured admin panel that allows administrators to conveniently manage files"
  },
  {
    "path": "docs/en/guide/security.md",
    "chars": 9898,
    "preview": "# Security Settings\n\nFileCodeBox provides multiple layers of security mechanisms to protect your file sharing service. T"
  },
  {
    "path": "docs/en/guide/share.md",
    "chars": 8271,
    "preview": "# File Sharing\n\nFileCodeBox provides simple and easy-to-use file and text sharing functionality. Users can securely shar"
  },
  {
    "path": "docs/en/guide/storage.md",
    "chars": 12551,
    "preview": "# Storage Configuration\n\nFileCodeBox supports multiple storage backends. You can choose the appropriate storage method b"
  },
  {
    "path": "docs/en/guide/upload.md",
    "chars": 11156,
    "preview": "# File Upload\n\nFileCodeBox provides multiple flexible file upload methods, supporting both regular upload and chunked up"
  },
  {
    "path": "docs/en/index.md",
    "chars": 1296,
    "preview": "---\nlayout: home\n\nhero:\n  name: \"FileCodeBox\"\n  text: \"File Express Box\"\n  tagline: Share text and files anonymously wit"
  },
  {
    "path": "docs/en/showcase.md",
    "chars": 2713,
    "preview": "# Showcase\n\nHere are some excellent sites built with FileCodeBox. If you've deployed FileCodeBox, feel free to submit a "
  },
  {
    "path": "docs/guide/configuration.md",
    "chars": 4328,
    "preview": "# 配置说明\n\nFileCodeBox 提供了丰富的配置选项,可以通过管理面板或直接修改配置来自定义系统行为。本文档详细介绍所有可用的配置项。\n\n## 配置方式\n\nFileCodeBox 支持两种配置方式:\n\n1. **管理面板配置**(推"
  },
  {
    "path": "docs/guide/getting-started.md",
    "chars": 1929,
    "preview": "# 快速开始\n\n## 简介\n\nFileCodeBox 是一个简单高效的文件分享工具,支持文件临时中转、分享和管理。本指南将帮助您快速部署和使用 FileCodeBox。\n\n## 特性\n\n- 🚀 快速部署:支持 Docker 一键部署\n- 🔒"
  },
  {
    "path": "docs/guide/introduction.md",
    "chars": 4373,
    "preview": "<div align=\"center\">\n\n<img src=\"https://fastly.jsdelivr.net/gh/vastsa/FileCodeBox@V1.6/static/banners/img_1.png\" alt=\"Fi"
  },
  {
    "path": "docs/guide/management.md",
    "chars": 6040,
    "preview": "# 管理面板\n\nFileCodeBox 提供了功能完善的管理面板,让管理员可以方便地管理文件、查看系统状态和修改配置。本文档介绍管理面板的各项功能和使用方法。\n\n## 访问管理面板\n\n### 登录方式\n\n管理面板位于 `/admin` 路径"
  },
  {
    "path": "docs/guide/security.md",
    "chars": 6099,
    "preview": "# 安全设置\n\nFileCodeBox 提供了多层安全机制来保护您的文件分享服务。本文档介绍如何正确配置安全选项,确保系统安全运行。\n\n## 管理员密码\n\n### 修改默认密码\n\n::: danger 重要安全警告\nFileCodeBox "
  },
  {
    "path": "docs/guide/share.md",
    "chars": 4831,
    "preview": "# 文件分享\n\nFileCodeBox 提供了简单易用的文件和文本分享功能。用户可以通过提取码安全地分享和获取文件。\n\n## 分享方式\n\nFileCodeBox 支持两种分享方式:\n\n1. **文本分享** - 直接分享文本内容,适合代码片"
  },
  {
    "path": "docs/guide/storage-onedrive.md",
    "chars": 4219,
    "preview": "# OneDrive作为存储的配置方法\n\n**仅支持工作或学校账户,并且需要有管理员权限以授权API**\n\n## 1. 需要配置的参数\n\n```\nfile_storage=onedrive\nonedrive_domain=XXXXXX\non"
  },
  {
    "path": "docs/guide/storage-opendal.md",
    "chars": 674,
    "preview": "# 通过 OpenDAL 集成存储的配置方法\n\n## 需要配置的参数\n\n```dotenv\nfile_storage=opendal\nopendal_scheme=<service_name>\nopendal_<service_name>_"
  },
  {
    "path": "docs/guide/storage.md",
    "chars": 8451,
    "preview": "# 存储配置\n\nFileCodeBox 支持多种存储后端,您可以根据需求选择合适的存储方式。本文档将详细介绍各种存储后端的配置方法。\n\n## 存储类型概览\n\n| 存储类型 | 配置值 | 说明 |\n|---------|--------|-"
  },
  {
    "path": "docs/guide/upload.md",
    "chars": 7381,
    "preview": "# 文件上传\n\nFileCodeBox 提供了多种灵活的文件上传方式,支持普通上传和分片上传,满足不同场景的需求。\n\n## 上传方式\n\nFileCodeBox 支持以下几种上传方式:\n\n### 拖拽上传\n\n将文件直接拖拽到上传区域即可开始上"
  },
  {
    "path": "docs/index.md",
    "chars": 798,
    "preview": "---\nlayout: home\n\nhero:\n  name: \"FileCodeBox\"\n  text: \"文件快递柜\"\n  tagline: 匿名口令分享文本,文件,像拿快递一样取文件\n  image:\n    src: /logo_s"
  },
  {
    "path": "docs/package.json",
    "chars": 186,
    "preview": "{\n  \"devDependencies\": {\n    \"vitepress\": \"^1.6.3\"\n  },\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev\",\n    \"docs:build\""
  },
  {
    "path": "docs/showcase.md",
    "chars": 1891,
    "preview": "# 优秀案例\n\n这里收录了一些使用 FileCodeBox 搭建的优秀站点。如果你也部署了 FileCodeBox,欢迎提交 PR 将你的站点添加到这里!\n\n## 官方演示站\n\n<div class=\"showcase-grid\">\n\n<d"
  },
  {
    "path": "main.py",
    "chars": 4915,
    "preview": "# @Time    : 2023/8/9 23:23\n# @Author  : Lan\n# @File    : main.py\n# @Software: PyCharm\nimport asyncio\nimport time\nfrom c"
  },
  {
    "path": "readme.md",
    "chars": 9383,
    "preview": "<div align=\"center\">\n\n# FileCodeBox\n\n### 文件快递柜 - 匿名口令分享文本和文件\n\n<img src=\"https://fastly.jsdelivr.net/gh/vastsa/FileCodeBo"
  },
  {
    "path": "readme_en.md",
    "chars": 12074,
    "preview": "<div align=\"center\">\n\n# FileCodeBox\n\n### Anonymous File & Text Sharing with Passcode\n\n<img src=\"https://fastly.jsdelivr."
  },
  {
    "path": "requirements.txt",
    "chars": 146,
    "preview": "aioboto3==15.5.0\naiohttp==3.13.3\naiofiles==25.1.0\nfastapi==0.128.0\npydantic==2.12.5\nuvicorn==0.40.0\ntortoise-orm==0.25.3"
  }
]

About this extraction

This page contains the full source code of the vastsa/FileCodeBox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 85 files (1.2 MB), approximately 347.3k tokens, and a symbol index with 1327 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!