添加前端页面以及相关打包脚本和内置 web 的逻辑
This commit is contained in:
40
build.bat
Normal file
40
build.bat
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@echo off
|
||||||
|
set APP_NAME=FileRelay.exe
|
||||||
|
set DIST_DIR=dist
|
||||||
|
set CONFIG_SRC=config\config.yaml
|
||||||
|
set CONFIG_DEST=%DIST_DIR%\config\config.yaml
|
||||||
|
|
||||||
|
echo 开始构建 %APP_NAME%...
|
||||||
|
|
||||||
|
:: 清理 dist 目录
|
||||||
|
if exist "%DIST_DIR%" (
|
||||||
|
echo 正在清理 %DIST_DIR% 目录...
|
||||||
|
rd /s /q "%DIST_DIR%"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 创建 dist 目录
|
||||||
|
if not exist "%DIST_DIR%\config" (
|
||||||
|
mkdir "%DIST_DIR%\config"
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 编译 Go 二进制文件
|
||||||
|
echo 正在编译...
|
||||||
|
go build -o "%DIST_DIR%\%APP_NAME%" main.go
|
||||||
|
|
||||||
|
if %ERRORLEVEL% equ 0 (
|
||||||
|
echo 编译成功!
|
||||||
|
) else (
|
||||||
|
echo 编译失败!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: 复制配置文件
|
||||||
|
if exist "%CONFIG_SRC%" (
|
||||||
|
echo 正在复制配置文件...
|
||||||
|
copy "%CONFIG_SRC%" "%CONFIG_DEST%" /Y
|
||||||
|
) else (
|
||||||
|
echo 警告: 未找到源配置文件 %CONFIG_SRC%,跳过复制。
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 打包完成!输出目录: %DIST_DIR%
|
||||||
|
echo 你可以运行 .\%DIST_DIR%\%APP_NAME% 来启动服务。
|
||||||
40
build.ps1
Normal file
40
build.ps1
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 设置变量
|
||||||
|
$APP_NAME = "FileRelay.exe"
|
||||||
|
$DIST_DIR = "dist"
|
||||||
|
$CONFIG_SRC = "config\config.yaml"
|
||||||
|
$CONFIG_DEST = "$DIST_DIR\config\config.yaml"
|
||||||
|
|
||||||
|
Write-Host "开始构建 $APP_NAME..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 清理 dist 目录
|
||||||
|
if (Test-Path $DIST_DIR) {
|
||||||
|
Write-Host "正在清理 $DIST_DIR 目录..."
|
||||||
|
Remove-Item -Path $DIST_DIR -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建 dist 目录
|
||||||
|
if (-not (Test-Path "$DIST_DIR\config")) {
|
||||||
|
New-Item -Path "$DIST_DIR\config" -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# 编译 Go 二进制文件
|
||||||
|
Write-Host "正在编译..."
|
||||||
|
go build -o "$DIST_DIR\$APP_NAME" main.go
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "编译成功!" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "编译失败!" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 复制配置文件
|
||||||
|
if (Test-Path $CONFIG_SRC) {
|
||||||
|
Write-Host "正在复制配置文件..."
|
||||||
|
Copy-Item -Path $CONFIG_SRC -Destination $CONFIG_DEST -Force
|
||||||
|
} else {
|
||||||
|
Write-Host "警告: 未找到源配置文件 $CONFIG_SRC,跳过复制。" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "打包完成!输出目录: $DIST_DIR" -ForegroundColor Green
|
||||||
|
Write-Host "你可以运行 .\$DIST_DIR\$APP_NAME 来启动服务。"
|
||||||
40
build.sh
Executable file
40
build.sh
Executable file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 设置变量
|
||||||
|
APP_NAME="FileRelay"
|
||||||
|
DIST_DIR="dist"
|
||||||
|
CONFIG_SRC="config/config.yaml"
|
||||||
|
CONFIG_DEST="$DIST_DIR/config/config.yaml"
|
||||||
|
|
||||||
|
echo "开始构建 $APP_NAME..."
|
||||||
|
|
||||||
|
# 清理 dist 目录
|
||||||
|
if [ -d "$DIST_DIR" ]; then
|
||||||
|
echo "正在清理 $DIST_DIR 目录..."
|
||||||
|
rm -rf "$DIST_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建 dist 目录
|
||||||
|
mkdir -p "$DIST_DIR/config"
|
||||||
|
|
||||||
|
# 编译 Go 二进制文件
|
||||||
|
echo "正在编译..."
|
||||||
|
go build -o "$DIST_DIR/$APP_NAME" main.go
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "编译成功!"
|
||||||
|
else
|
||||||
|
echo "编译失败!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 复制配置文件
|
||||||
|
if [ -f "$CONFIG_SRC" ]; then
|
||||||
|
echo "正在复制配置文件..."
|
||||||
|
cp "$CONFIG_SRC" "$CONFIG_DEST"
|
||||||
|
else
|
||||||
|
echo "警告: 未找到源配置文件 $CONFIG_SRC,跳过复制。"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "打包完成!输出目录: $DIST_DIR"
|
||||||
|
echo "你可以运行 ./$DIST_DIR/$APP_NAME 来启动服务。"
|
||||||
@@ -34,3 +34,5 @@ api_token:
|
|||||||
max_tokens: 20
|
max_tokens: 20
|
||||||
database:
|
database:
|
||||||
path: file_relay.db
|
path: file_relay.db
|
||||||
|
web:
|
||||||
|
path: web
|
||||||
|
|||||||
64
docs/docs.go
64
docs/docs.go
@@ -855,7 +855,7 @@ const docTemplate = `{
|
|||||||
"APITokenAuth": []
|
"APITokenAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。",
|
"description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
],
|
],
|
||||||
@@ -893,6 +893,58 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/files/{file_id}/{filename}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"APITokenAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
|
||||||
|
"produces": [
|
||||||
|
"application/octet-stream"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Public"
|
||||||
|
],
|
||||||
|
"summary": "下载单个文件",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件 ID (UUID)",
|
||||||
|
"name": "file_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件名",
|
||||||
|
"name": "filename",
|
||||||
|
"in": "path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"410": {
|
||||||
|
"description": "Gone",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
@@ -969,6 +1021,9 @@ const docTemplate = `{
|
|||||||
"admin.UpdateBatchRequest": {
|
"admin.UpdateBatchRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"download_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"expire_at": {
|
"expire_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1068,6 +1123,10 @@ const docTemplate = `{
|
|||||||
"config.SecurityConfig": {
|
"config.SecurityConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"admin_password": {
|
||||||
|
"description": "管理员密码明文 (仅用于更新请求,不保存到文件)",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"admin_password_hash": {
|
"admin_password_hash": {
|
||||||
"description": "管理员密码哈希 (bcrypt)",
|
"description": "管理员密码哈希 (bcrypt)",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1277,6 +1336,9 @@ const docTemplate = `{
|
|||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"download_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -848,7 +848,7 @@
|
|||||||
"APITokenAuth": []
|
"APITokenAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。",
|
"description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
],
|
],
|
||||||
@@ -886,6 +886,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/files/{file_id}/{filename}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"APITokenAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。",
|
||||||
|
"produces": [
|
||||||
|
"application/octet-stream"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Public"
|
||||||
|
],
|
||||||
|
"summary": "下载单个文件",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件 ID (UUID)",
|
||||||
|
"name": "file_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "文件名",
|
||||||
|
"name": "filename",
|
||||||
|
"in": "path"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"410": {
|
||||||
|
"description": "Gone",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/model.Response"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
@@ -962,6 +1014,9 @@
|
|||||||
"admin.UpdateBatchRequest": {
|
"admin.UpdateBatchRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"download_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"expire_at": {
|
"expire_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1061,6 +1116,10 @@
|
|||||||
"config.SecurityConfig": {
|
"config.SecurityConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"admin_password": {
|
||||||
|
"description": "管理员密码明文 (仅用于更新请求,不保存到文件)",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"admin_password_hash": {
|
"admin_password_hash": {
|
||||||
"description": "管理员密码哈希 (bcrypt)",
|
"description": "管理员密码哈希 (bcrypt)",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1270,6 +1329,9 @@
|
|||||||
"created_at": {
|
"created_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"download_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
admin.UpdateBatchRequest:
|
admin.UpdateBatchRequest:
|
||||||
properties:
|
properties:
|
||||||
|
download_count:
|
||||||
|
type: integer
|
||||||
expire_at:
|
expire_at:
|
||||||
type: string
|
type: string
|
||||||
expire_type:
|
expire_type:
|
||||||
@@ -106,6 +108,9 @@ definitions:
|
|||||||
type: object
|
type: object
|
||||||
config.SecurityConfig:
|
config.SecurityConfig:
|
||||||
properties:
|
properties:
|
||||||
|
admin_password:
|
||||||
|
description: 管理员密码明文 (仅用于更新请求,不保存到文件)
|
||||||
|
type: string
|
||||||
admin_password_hash:
|
admin_password_hash:
|
||||||
description: 管理员密码哈希 (bcrypt)
|
description: 管理员密码哈希 (bcrypt)
|
||||||
type: string
|
type: string
|
||||||
@@ -251,6 +256,8 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
|
download_url:
|
||||||
|
type: string
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
mime_type:
|
mime_type:
|
||||||
@@ -854,9 +861,42 @@ paths:
|
|||||||
summary: 获取公共配置
|
summary: 获取公共配置
|
||||||
tags:
|
tags:
|
||||||
- Public
|
- Public
|
||||||
|
/api/files/{file_id}/{filename}:
|
||||||
|
get:
|
||||||
|
description: 根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
|
||||||
|
parameters:
|
||||||
|
- description: 文件 ID (UUID)
|
||||||
|
in: path
|
||||||
|
name: file_id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: 文件名
|
||||||
|
in: path
|
||||||
|
name: filename
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/octet-stream
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
type: file
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Response'
|
||||||
|
"410":
|
||||||
|
description: Gone
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/model.Response'
|
||||||
|
security:
|
||||||
|
- APITokenAuth: []
|
||||||
|
summary: 下载单个文件
|
||||||
|
tags:
|
||||||
|
- Public
|
||||||
/api/files/{file_id}/download:
|
/api/files/{file_id}/download:
|
||||||
get:
|
get:
|
||||||
description: 根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。
|
description: 根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
|
||||||
parameters:
|
parameters:
|
||||||
- description: 文件 ID (UUID)
|
- description: 文件 ID (UUID)
|
||||||
in: path
|
in: path
|
||||||
|
|||||||
@@ -115,6 +115,20 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成文件下载绝对路径直链
|
||||||
|
scheme := "http"
|
||||||
|
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
host := c.Request.Host
|
||||||
|
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
|
||||||
|
host = forwardedHost
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range batch.FileItems {
|
||||||
|
batch.FileItems[i].DownloadURL = fmt.Sprintf("%s://%s/api/files/%s/%s", scheme, host, batch.FileItems[i].ID, batch.FileItems[i].OriginalName)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{
|
c.JSON(http.StatusOK, model.SuccessResponse(PickupResponse{
|
||||||
Remark: batch.Remark,
|
Remark: batch.Remark,
|
||||||
ExpireAt: batch.ExpireAt,
|
ExpireAt: batch.ExpireAt,
|
||||||
@@ -129,14 +143,16 @@ func (h *PickupHandler) Pickup(c *gin.Context) {
|
|||||||
|
|
||||||
// DownloadFile 下载单个文件
|
// DownloadFile 下载单个文件
|
||||||
// @Summary 下载单个文件
|
// @Summary 下载单个文件
|
||||||
// @Description 根据文件 ID 下载单个文件。可选提供带 pickup scope 的 API Token。
|
// @Description 根据文件 ID 下载单个文件。支持直观的文件名结尾以方便下载工具识别。可选提供带 pickup scope 的 API Token。
|
||||||
// @Tags Public
|
// @Tags Public
|
||||||
// @Security APITokenAuth
|
// @Security APITokenAuth
|
||||||
// @Param file_id path string true "文件 ID (UUID)"
|
// @Param file_id path string true "文件 ID (UUID)"
|
||||||
|
// @Param filename path string false "文件名"
|
||||||
// @Produce application/octet-stream
|
// @Produce application/octet-stream
|
||||||
// @Success 200 {file} file
|
// @Success 200 {file} file
|
||||||
// @Failure 404 {object} model.Response
|
// @Failure 404 {object} model.Response
|
||||||
// @Failure 410 {object} model.Response
|
// @Failure 410 {object} model.Response
|
||||||
|
// @Router /api/files/{file_id}/{filename} [get]
|
||||||
// @Router /api/files/{file_id}/download [get]
|
// @Router /api/files/{file_id}/download [get]
|
||||||
func (h *PickupHandler) DownloadFile(c *gin.Context) {
|
func (h *PickupHandler) DownloadFile(c *gin.Context) {
|
||||||
fileID := c.Param("file_id")
|
fileID := c.Param("file_id")
|
||||||
@@ -155,7 +171,12 @@ func (h *PickupHandler) DownloadFile(c *gin.Context) {
|
|||||||
|
|
||||||
if h.batchService.IsExpired(&batch) {
|
if h.batchService.IsExpired(&batch) {
|
||||||
h.batchService.MarkAsExpired(&batch)
|
h.batchService.MarkAsExpired(&batch)
|
||||||
|
// 按照需求,如果不存在(已在上面处理)或达到上限,返回 404
|
||||||
|
if batch.ExpireType == "download" && batch.DownloadCount >= batch.MaxDownloads {
|
||||||
|
c.JSON(http.StatusNotFound, model.ErrorResponse(model.CodeNotFound, "file not found or download limit reached"))
|
||||||
|
} else {
|
||||||
c.JSON(http.StatusGone, model.ErrorResponse(model.CodeGone, "batch expired"))
|
c.JSON(http.StatusGone, model.ErrorResponse(model.CodeGone, "batch expired"))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -14,6 +15,11 @@ type Config struct {
|
|||||||
Storage StorageConfig `yaml:"storage" json:"storage"` // 存储设置
|
Storage StorageConfig `yaml:"storage" json:"storage"` // 存储设置
|
||||||
APIToken APITokenConfig `yaml:"api_token" json:"api_token"` // API Token 设置
|
APIToken APITokenConfig `yaml:"api_token" json:"api_token"` // API Token 设置
|
||||||
Database DatabaseConfig `yaml:"database" json:"database"` // 数据库设置
|
Database DatabaseConfig `yaml:"database" json:"database"` // 数据库设置
|
||||||
|
Web WebConfig `yaml:"web" json:"web"` // Web 前端设置
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebConfig struct {
|
||||||
|
Path string `yaml:"path" json:"path"` // Web 前端资源路径
|
||||||
}
|
}
|
||||||
|
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
@@ -78,6 +84,26 @@ func LoadConfig(path string) error {
|
|||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
// 创建默认配置
|
||||||
|
cfg := GetDefaultConfig()
|
||||||
|
data, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 确保目录存在
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
GlobalConfig = cfg
|
||||||
|
ConfigPath = path
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -93,6 +119,47 @@ func LoadConfig(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Site: SiteConfig{
|
||||||
|
Name: "文件暂存柜",
|
||||||
|
Description: "临时文件中转服务",
|
||||||
|
Logo: "https://www.hxuanyu.com/upload/favicon.png",
|
||||||
|
},
|
||||||
|
Security: SecurityConfig{
|
||||||
|
AdminPasswordHash: "$2a$10$Bm0TEmU4uj.bVHYiIPFBheUkcdg6XHpsanLvmpoAtgU1UnKbo9.vy", // 默认密码: admin123
|
||||||
|
PickupCodeLength: 6,
|
||||||
|
PickupFailLimit: 5,
|
||||||
|
JWTSecret: "file-relay-secret",
|
||||||
|
},
|
||||||
|
Upload: UploadConfig{
|
||||||
|
MaxFileSizeMB: 100,
|
||||||
|
MaxBatchFiles: 20,
|
||||||
|
MaxRetentionDays: 30,
|
||||||
|
RequireToken: false,
|
||||||
|
},
|
||||||
|
Storage: StorageConfig{
|
||||||
|
Type: "local",
|
||||||
|
Local: struct {
|
||||||
|
Path string `yaml:"path" json:"path"`
|
||||||
|
}{
|
||||||
|
Path: "storage_data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
APIToken: APITokenConfig{
|
||||||
|
Enabled: true,
|
||||||
|
AllowAdminAPI: true,
|
||||||
|
MaxTokens: 20,
|
||||||
|
},
|
||||||
|
Database: DatabaseConfig{
|
||||||
|
Path: "file_relay.db",
|
||||||
|
},
|
||||||
|
Web: WebConfig{
|
||||||
|
Path: "web",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func SaveConfig() error {
|
func SaveConfig() error {
|
||||||
configLock.RLock()
|
configLock.RLock()
|
||||||
defer configLock.RUnlock()
|
defer configLock.RUnlock()
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ type FileItem struct {
|
|||||||
StoragePath string `json:"storage_path"`
|
StoragePath string `json:"storage_path"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
MimeType string `json:"mime_type"`
|
MimeType string `json:"mime_type"`
|
||||||
|
DownloadURL string `gorm:"-" json:"download_url,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
127
main.go
127
main.go
@@ -10,8 +10,17 @@ import (
|
|||||||
"FileRelay/internal/model"
|
"FileRelay/internal/model"
|
||||||
"FileRelay/internal/task"
|
"FileRelay/internal/task"
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -19,6 +28,9 @@ import (
|
|||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed all:web
|
||||||
|
var webFS embed.FS
|
||||||
|
|
||||||
// @title 文件暂存柜 API
|
// @title 文件暂存柜 API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @description 自托管的文件暂存柜后端系统 API 文档
|
// @description 自托管的文件暂存柜后端系统 API 文档
|
||||||
@@ -44,11 +56,50 @@ import (
|
|||||||
// @description Type "Bearer <API-Token>" to authenticate. Required scope depends on the endpoint.
|
// @description Type "Bearer <API-Token>" to authenticate. Required scope depends on the endpoint.
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// 注册常用 MIME 类型
|
||||||
|
mime.AddExtensionType(".js", "application/javascript")
|
||||||
|
mime.AddExtensionType(".css", "text/css")
|
||||||
|
mime.AddExtensionType(".woff", "font/woff")
|
||||||
|
mime.AddExtensionType(".woff2", "font/woff2")
|
||||||
|
mime.AddExtensionType(".svg", "image/svg+xml")
|
||||||
|
|
||||||
|
// 解析命令行参数
|
||||||
|
configPath := flag.String("config", "config/config.yaml", "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
// 1. 加载配置
|
// 1. 加载配置
|
||||||
if err := config.LoadConfig("config/config.yaml"); err != nil {
|
if err := config.LoadConfig(*configPath); err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port := 8080
|
||||||
|
|
||||||
|
// 打印配置信息
|
||||||
|
fmt.Println("========================================")
|
||||||
|
fmt.Println("FileRelay 服务启动中...")
|
||||||
|
fmt.Printf("配置文件路径: %s\n", *configPath)
|
||||||
|
fmt.Printf("监听端口: %d\n", port)
|
||||||
|
fmt.Printf("数据库文件: %s\n", config.GlobalConfig.Database.Path)
|
||||||
|
fmt.Printf("存储模式: %s\n", config.GlobalConfig.Storage.Type)
|
||||||
|
|
||||||
|
webPath := config.GlobalConfig.Web.Path
|
||||||
|
useExternalWeb := false
|
||||||
|
if webPath != "" {
|
||||||
|
if info, err := os.Stat(webPath); err == nil && info.IsDir() {
|
||||||
|
useExternalWeb = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if useExternalWeb {
|
||||||
|
fmt.Printf("前端资源来源: 外部目录 (%s)\n", webPath)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("前端资源来源: 内置嵌入 (嵌入 fs)\n")
|
||||||
|
if webPath != "" {
|
||||||
|
fmt.Printf("提示: 配置的外部前端路径 %s 不存在,已回退到内置资源\n", webPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("========================================")
|
||||||
|
|
||||||
// 2. 初始化
|
// 2. 初始化
|
||||||
bootstrap.InitDB()
|
bootstrap.InitDB()
|
||||||
|
|
||||||
@@ -82,7 +133,9 @@ func main() {
|
|||||||
api.POST("/batches/text", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.UploadText)
|
api.POST("/batches/text", middleware.APITokenAuth(model.ScopeUpload, !config.GlobalConfig.Upload.RequireToken), uploadHandler.UploadText)
|
||||||
api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.Pickup)
|
api.GET("/batches/:pickup_code", middleware.PickupRateLimit(), middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.Pickup)
|
||||||
api.GET("/batches/:pickup_code/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadBatch)
|
api.GET("/batches/:pickup_code/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadBatch)
|
||||||
// 文件下载保持 /files/:id/download 风格
|
// 文件下载路由,支持直观的文件名结尾
|
||||||
|
api.GET("/files/:file_id/:filename", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
|
||||||
|
// 保持旧路由兼容性
|
||||||
api.GET("/files/:file_id/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
|
api.GET("/files/:file_id/download", middleware.APITokenAuth(model.ScopePickup, true), pickupHandler.DownloadFile)
|
||||||
|
|
||||||
// 保持旧路由兼容性 (可选,但为了平滑过渡通常建议保留一段时间或直接更新)
|
// 保持旧路由兼容性 (可选,但为了平滑过渡通常建议保留一段时间或直接更新)
|
||||||
@@ -114,8 +167,74 @@ func main() {
|
|||||||
adm.POST("/api-tokens/:id/revoke", tokenHandler.RevokeToken)
|
adm.POST("/api-tokens/:id/revoke", tokenHandler.RevokeToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 静态资源服务 (放在最后,确保 API 路由优先)
|
||||||
|
webSub, _ := fs.Sub(webFS, "web")
|
||||||
|
|
||||||
|
r.NoRoute(func(c *gin.Context) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
|
// 如果请求的是 API 或 Swagger,则不处理静态资源 (让其返回 404)
|
||||||
|
// 注意:此处不排除 /admin,因为 /admin 通常是前端 SPA 的路由地址
|
||||||
|
if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/swagger") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:尝试从外部或嵌入服务文件
|
||||||
|
serveFile := func(relPath string, allowExternal bool) bool {
|
||||||
|
// 1. 优先尝试外部路径
|
||||||
|
if allowExternal && config.GlobalConfig.Web.Path != "" {
|
||||||
|
fullPath := filepath.Join(config.GlobalConfig.Web.Path, relPath)
|
||||||
|
if info, err := os.Stat(fullPath); err == nil && !info.IsDir() {
|
||||||
|
c.File(fullPath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试嵌入式文件
|
||||||
|
f, err := webSub.Open(relPath)
|
||||||
|
if err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
stat, err := f.Stat()
|
||||||
|
if err == nil && !stat.IsDir() {
|
||||||
|
// 使用 http.ServeContent 避免 c.FileFromFS 重定向问题
|
||||||
|
if rs, ok := f.(io.ReadSeeker); ok {
|
||||||
|
// 显式设置 Content-Type,防止某些环境下识别失败
|
||||||
|
ext := filepath.Ext(relPath)
|
||||||
|
ctype := mime.TypeByExtension(ext)
|
||||||
|
if ctype != "" {
|
||||||
|
c.Header("Content-Type", ctype)
|
||||||
|
}
|
||||||
|
http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), rs)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 尝试直接请求的文件 (如果是 / 则尝试 index.html)
|
||||||
|
requestedPath := strings.TrimPrefix(path, "/")
|
||||||
|
if requestedPath == "" {
|
||||||
|
requestedPath = "index.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
if serveFile(requestedPath, true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SPA 支持:对于非文件请求(没有后缀或不包含点),尝试返回 index.html
|
||||||
|
// 如果请求的是 assets 目录下的文件(包含点)却没找到,不应该回退到 index.html
|
||||||
|
isAsset := strings.Contains(requestedPath, ".")
|
||||||
|
if !isAsset || requestedPath == "index.html" {
|
||||||
|
if serveFile("index.html", true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终找不到则返回 404
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
// 5. 运行
|
// 5. 运行
|
||||||
port := 8080
|
|
||||||
fmt.Printf("Server is running on port %d\n", port)
|
|
||||||
r.Run(fmt.Sprintf(":%d", port))
|
r.Run(fmt.Sprintf(":%d", port))
|
||||||
}
|
}
|
||||||
|
|||||||
1
web/assets/AdminDashboard-B71bpyA0.js
Normal file
1
web/assets/AdminDashboard-B71bpyA0.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/AdminDashboard-D-aUQJ-6.css
Normal file
1
web/assets/AdminDashboard-D-aUQJ-6.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.border-b-2[data-v-3adbfb80]{border-bottom-width:2px}.hover\:border-gray-300[data-v-3adbfb80]:hover{border-color:#d1d5db}.hover\:text-gray-700[data-v-3adbfb80]:hover{color:#374151}.hover\:shadow-lg[data-v-3adbfb80]:hover{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d;transition:box-shadow .3s ease}.flex-col[data-v-3adbfb80]:hover{transform:translateY(-2px);transition:transform .2s ease}.hover\:bg-gray-50[data-v-3adbfb80]:hover{background-color:#f9fafb;transition:background-color .2s ease}@keyframes slideInUp-3adbfb80{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.grid[data-v-3adbfb80]>*{animation:slideInUp-3adbfb80 .3s ease forwards}.grid[data-v-3adbfb80]>*:nth-child(1){animation-delay:.1s}.grid[data-v-3adbfb80]>*:nth-child(2){animation-delay:.2s}.grid[data-v-3adbfb80]>*:nth-child(3){animation-delay:.3s}.grid[data-v-3adbfb80]>*:nth-child(4){animation-delay:.4s}.overflow-x-auto[data-v-3adbfb80]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar{height:6px}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar-track{background:transparent}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:3px}.overflow-x-auto[data-v-3adbfb80]::-webkit-scrollbar-thumb:hover{background:#9ca3afb3}
|
||||||
File diff suppressed because one or more lines are too long
1
web/assets/AdminLogin-BBr6yhKu.css
Normal file
1
web/assets/AdminLogin-BBr6yhKu.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.animate-spin[data-v-266683e4]{animation:spin-266683e4 1s linear infinite}@keyframes spin-266683e4{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.group:hover .group-hover\:text-indigo-400[data-v-266683e4]{color:#818cf8}input[type=password][data-v-266683e4]::-ms-reveal,input[type=password][data-v-266683e4]::-ms-clear{display:none}.shadow-lg[data-v-266683e4]{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.hover\:bg-indigo-700[data-v-266683e4]:hover{background-color:#4338ca;transition:background-color .2s ease}.bg-red-50[data-v-266683e4]{animation:fadeIn-266683e4 .3s ease-in-out}@keyframes fadeIn-266683e4{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.focus\:ring-2[data-v-266683e4]:focus{outline:none;box-shadow:0 0 0 2px #6366f133}.focus\:ring-offset-2[data-v-266683e4]:focus{outline:none;box-shadow:0 0 0 2px #fff,0 0 0 4px #6366f133}
|
||||||
1
web/assets/AdminLogin-DALRrCFv.js
Normal file
1
web/assets/AdminLogin-DALRrCFv.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/BatchManagement-C0ramMV5.css
Normal file
1
web/assets/BatchManagement-C0ramMV5.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.border-b-2[data-v-18610c62]{border-bottom-width:2px}.hover\:border-gray-300[data-v-18610c62]:hover{border-color:#d1d5db}.hover\:text-gray-700[data-v-18610c62]:hover{color:#374151}.hover\:bg-gray-50[data-v-18610c62]:hover{background-color:#f9fafb;transition:background-color .2s ease}.overflow-x-auto[data-v-18610c62],.overflow-y-auto[data-v-18610c62]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar{width:6px;height:6px}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar-track,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar-track{background:transparent}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar-thumb,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:3px}.overflow-x-auto[data-v-18610c62]::-webkit-scrollbar-thumb:hover,.overflow-y-auto[data-v-18610c62]::-webkit-scrollbar-thumb:hover{background:#9ca3afb3}.animate-spin[data-v-18610c62]{animation:spin-18610c62 1s linear infinite}@keyframes spin-18610c62{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.space-y-4[data-v-18610c62]>*{animation:fadeInUp-18610c62 .3s ease forwards}@keyframes fadeInUp-18610c62{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.hover\:scale-105[data-v-18610c62]:hover{transform:scale(1.05);transition:transform .2s ease}
|
||||||
1
web/assets/BatchManagement-CeACwrQ7.js
Normal file
1
web/assets/BatchManagement-CeACwrQ7.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{c as r}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as e,f as o,n as c,u as n,k as d,g as l}from"./index-BhaIiTEj.js";const m=e({__name:"CardDescription",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),o("p",{"data-slot":"card-description",class:c(n(r)("text-muted-foreground text-sm",a.class))},[d(t.$slots,"default")],2))}}),_=e({__name:"CardHeader",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),o("div",{"data-slot":"card-header",class:c(n(r)("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",a.class))},[d(t.$slots,"default")],2))}}),f=e({__name:"CardTitle",props:{class:{}},setup(s){const a=s;return(t,p)=>(l(),o("h3",{"data-slot":"card-title",class:c(n(r)("leading-none font-semibold",a.class))},[d(t.$slots,"default")],2))}});export{_,f as a,m as b};
|
||||||
1
web/assets/ConfigManagement-DKtU2SIn.js
Normal file
1
web/assets/ConfigManagement-DKtU2SIn.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
20
web/assets/HomePage-7MZxEK4U.js
Normal file
20
web/assets/HomePage-7MZxEK4U.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/HomePage-CzsVSF8w.css
Normal file
1
web/assets/HomePage-CzsVSF8w.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.container[data-v-72019359]{max-width:1200px}.tracking-widest[data-v-72019359]{letter-spacing:.1em}.animate-spin[data-v-72019359]{animation:spin-72019359 1s linear infinite}@keyframes spin-72019359{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.shadow-lg[data-v-72019359]{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.hover\:bg-gray-100[data-v-72019359]:hover{transition:background-color .2s ease}.overflow-y-auto[data-v-72019359]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-y-auto[data-v-72019359]::-webkit-scrollbar{width:8px}.overflow-y-auto[data-v-72019359]::-webkit-scrollbar-track{background:transparent}.overflow-y-auto[data-v-72019359]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:4px}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{d as l,N as n,_ as d,u as r,f as u,n as m,X as p,g as f}from"./index-BhaIiTEj.js";import{u as c}from"./Label.vue_vue_type_script_setup_true_lang-D-_Evs0_.js";import{c as b}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";const _=l({__name:"Input",props:{defaultValue:{},modelValue:{},class:{}},emits:["update:modelValue"],setup(a,{emit:o}){const e=a,t=c(e,"modelValue",o,{passive:!0,defaultValue:e.defaultValue});return(v,i)=>n((f(),u("input",{"onUpdate:modelValue":i[0]||(i[0]=s=>p(t)?t.value=s:null),"data-slot":"input",class:m(r(b)("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm","focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]","aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",e.class))},null,2)),[[d,r(t)]])}});export{_};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{P as B,m as E,i as F,j as L,r as N,c as q}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as v,h as y,g as C,z as S,k as O,l as P,u as m,a7 as D,v as J,w as x,c as M,s as T,M as z}from"./index-BhaIiTEj.js";import{u as R}from"./useForwardExpose-CRFbVhil.js";var U=v({__name:"Label",props:{for:{type:String,required:!1},asChild:{type:Boolean,required:!1},as:{type:null,required:!1,default:"label"}},setup(t){const e=t;return R(),(s,l)=>(C(),y(m(B),P(e,{onMousedown:l[0]||(l[0]=n=>{!n.defaultPrevented&&n.detail>1&&n.preventDefault()})}),{default:S(()=>[O(s.$slots,"default")]),_:3},16))}}),j=U;function H(t={}){const{inheritAttrs:e=!0}=t,s=T(),l=v({setup(o,{slots:u}){return()=>{s.value=u.default}}}),n=v({inheritAttrs:e,props:t.props,setup(o,{attrs:u,slots:_}){return()=>{var d;if(!s.value)throw new Error("[VueUse] Failed to find the definition of reusable template");const i=(d=s.value)===null||d===void 0?void 0:d.call(s,{...t.props==null?k(u):o,$slots:_});return e&&i?.length===1?i[0]:i}}});return E({define:l,reuse:n},[l,n])}function k(t){const e={};for(const s in t)e[L(s)]=t[s];return e}function A(t){return JSON.parse(JSON.stringify(t))}function Q(t,e,s,l={}){var n,o;const{clone:u=!1,passive:_=!1,eventName:d,deep:i=!1,defaultValue:V,shouldEmit:g}=l,a=D(),b=s||a?.emit||(a==null||(n=a.$emit)===null||n===void 0?void 0:n.bind(a))||(a==null||(o=a.proxy)===null||o===void 0||(o=o.$emit)===null||o===void 0?void 0:o.bind(a?.proxy));let c=d;e||(e="modelValue"),c=c||`update:${e.toString()}`;const h=r=>u?typeof u=="function"?u(r):A(r):r,$=()=>F(t[e])?h(t[e]):V,w=r=>{g?g(r)&&b(c,r):b(c,r)};if(_){const r=J($());let f=!1;return x(()=>t[e],p=>{f||(f=!0,r.value=h(p),z(()=>f=!1))}),x(r,p=>{!f&&(p!==t[e]||i)&&w(p)},{deep:i}),r}else return M({get(){return $()},set(r){w(r)}})}const W=v({__name:"Label",props:{for:{},asChild:{type:Boolean},as:{},class:{}},setup(t){const e=t,s=N(e,"class");return(l,n)=>(C(),y(m(j),P({"data-slot":"label"},m(s),{class:m(q)("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",e.class)}),{default:S(()=>[O(l.$slots,"default")]),_:3},16,["class"]))}});export{W as _,H as c,Q as u};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{d as f,f as r,j as e,I as n,R as _,z as a,E as g,g as i,u as t,i as k,D as d,n as u,C as h}from"./index-BhaIiTEj.js";import{d as m}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";const x={class:"bg-white border-b border-gray-200 sticky top-0 z-10"},v={class:"container mx-auto px-4"},w={class:"flex justify-between items-center h-16"},y={class:"flex items-center space-x-4"},b={key:0,class:"w-8 h-8 rounded-lg overflow-hidden flex items-center justify-center"},C=["src","alt"],j={key:1,class:"w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center"},B={class:"text-lg font-semibold text-gray-900"},z={key:0,class:"text-xs text-gray-500"},V={class:"flex items-center space-x-1"},D=f({__name:"NavBar",props:{showDescription:{type:Boolean}},setup(p){const{config:o}=g();return(c,s)=>{const l=_("router-link");return i(),r("nav",x,[e("div",v,[e("div",w,[e("div",y,[n(l,{to:"/",class:"flex items-center space-x-3"},{default:a(()=>[t(o).site?.logo?(i(),r("div",b,[e("img",{src:t(o).site.logo,alt:t(o).site?.name||"文件中转站",class:"w-full h-full object-contain"},null,8,C)])):(i(),r("div",j,[...s[0]||(s[0]=[e("svg",{class:"w-5 h-5 text-white",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"})],-1)])])),e("div",null,[e("h1",B,d(t(o).site?.name||"文件中转站"),1),p.showDescription?(i(),r("p",z,d(t(o).site?.description||"安全、便捷的文件暂存服务"),1)):k("",!0)])]),_:1})]),e("div",V,[n(l,{to:"/"},{default:a(()=>[n(t(m),{variant:"ghost",size:"sm",class:u(c.$route.path==="/"?"bg-gray-100":"")},{default:a(()=>[...s[1]||(s[1]=[e("svg",{class:"w-4 h-4 mr-2",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"}),e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M8 5l4-4 4 4"})],-1),h(" 取件 ",-1)])]),_:1},8,["class"])]),_:1}),n(l,{to:"/upload"},{default:a(()=>[n(t(m),{variant:"ghost",size:"sm",class:u(c.$route.path==="/upload"?"bg-gray-100":"")},{default:a(()=>[...s[2]||(s[2]=[e("svg",{class:"w-4 h-4 mr-2",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"})],-1),h(" 发送 ",-1)])]),_:1},8,["class"])]),_:1})])])])])}}});export{D as _};
|
||||||
File diff suppressed because one or more lines are too long
1
web/assets/PopperContent-MQwziWLW.js
Normal file
1
web/assets/PopperContent-MQwziWLW.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{c as t}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as l,f as o,n as r,u as c,g as n,j as _,k as d}from"./index-BhaIiTEj.js";const i=l({__name:"Skeleton",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("div",{"data-slot":"skeleton",class:r(c(t)("animate-pulse rounded-md bg-primary/10",a.class))},null,2))}}),u={"data-slot":"table-container",class:"relative w-full overflow-auto"},f=l({__name:"Table",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("div",u,[_("table",{"data-slot":"table",class:r(c(t)("w-full caption-bottom text-sm",a.class))},[d(e.$slots,"default")],2)]))}}),h=l({__name:"TableBody",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("tbody",{"data-slot":"table-body",class:r(c(t)("[&_tr:last-child]:border-0",a.class))},[d(e.$slots,"default")],2))}}),x=l({__name:"TableCell",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("td",{"data-slot":"table-cell",class:r(c(t)("p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",a.class))},[d(e.$slots,"default")],2))}}),$=l({__name:"TableRow",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("tr",{"data-slot":"table-row",class:r(c(t)("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",a.class))},[d(e.$slots,"default")],2))}}),k=l({__name:"TableHead",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("th",{"data-slot":"table-head",class:r(c(t)("text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",a.class))},[d(e.$slots,"default")],2))}}),w=l({__name:"TableHeader",props:{class:{}},setup(s){const a=s;return(e,p)=>(n(),o("thead",{"data-slot":"table-header",class:r(c(t)("[&_tr]:border-b",a.class))},[d(e.$slots,"default")],2))}});export{i as _,f as a,w as b,$ as c,k as d,h as e,x as f};
|
||||||
3
web/assets/Teleport-DV_BGdH0.js
Normal file
3
web/assets/Teleport-DV_BGdH0.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{d as l,N as n,f as d,n as u,u as s,X as m,_ as c,g as p}from"./index-BhaIiTEj.js";import{u as f}from"./Label.vue_vue_type_script_setup_true_lang-D-_Evs0_.js";import{c as x}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";const w=l({__name:"Textarea",props:{class:{},defaultValue:{},modelValue:{}},emits:["update:modelValue"],setup(r,{emit:o}){const e=r,a=f(e,"modelValue",o,{passive:!0,defaultValue:e.defaultValue});return(v,t)=>n((p(),d("textarea",{"onUpdate:modelValue":t[0]||(t[0]=i=>m(a)?a.value=i:null),"data-slot":"textarea",class:u(s(x)("border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",e.class))},null,2)),[[c,s(a)]])}});export{w as _};
|
||||||
1
web/assets/TokenManagement-BhQjUgic.css
Normal file
1
web/assets/TokenManagement-BhQjUgic.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.border-b-2[data-v-c7a1641d]{border-bottom-width:2px}.hover\:border-gray-300[data-v-c7a1641d]:hover{border-color:#d1d5db}.hover\:text-gray-700[data-v-c7a1641d]:hover{color:#374151}.hover\:bg-gray-50[data-v-c7a1641d]:hover{background-color:#f9fafb;transition:background-color .2s ease}code[data-v-c7a1641d]{word-break:break-all;white-space:pre-wrap}.overflow-x-auto[data-v-c7a1641d]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar{width:6px;height:6px}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar-track{background:transparent}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:3px}.overflow-x-auto[data-v-c7a1641d]::-webkit-scrollbar-thumb:hover{background:#9ca3afb3}.animate-spin[data-v-c7a1641d]{animation:spin-c7a1641d 1s linear infinite}@keyframes spin-c7a1641d{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.space-y-4[data-v-c7a1641d]>*{animation:fadeInUp-c7a1641d .3s ease forwards}@keyframes fadeInUp-c7a1641d{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.bg-yellow-50[data-v-c7a1641d]{animation:slideInDown-c7a1641d .3s ease-out}@keyframes slideInDown-c7a1641d{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}.bg-blue-50[data-v-c7a1641d]{animation:fadeIn-c7a1641d .3s ease-in-out}@keyframes fadeIn-c7a1641d{0%{opacity:0}to{opacity:1}}
|
||||||
1
web/assets/TokenManagement-Cwst5YRl.js
Normal file
1
web/assets/TokenManagement-Cwst5YRl.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/UploadPage-CvW4R7Fy.css
Normal file
1
web/assets/UploadPage-CvW4R7Fy.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.container[data-v-4101ad79]{max-width:1200px}.border-blue-400[data-v-4101ad79]{border-color:#60a5fa}.bg-blue-50[data-v-4101ad79]{background-color:#eff6ff}.animate-spin[data-v-4101ad79]{animation:spin-4101ad79 1s linear infinite}@keyframes spin-4101ad79{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.shadow-lg[data-v-4101ad79]{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.overflow-y-auto[data-v-4101ad79]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.5) transparent}.overflow-y-auto[data-v-4101ad79]::-webkit-scrollbar{width:8px}.overflow-y-auto[data-v-4101ad79]::-webkit-scrollbar-track{background:transparent}.overflow-y-auto[data-v-4101ad79]::-webkit-scrollbar-thumb{background:#9ca3af80;border-radius:4px}
|
||||||
6
web/assets/UploadPage-t63bWSjh.js
Normal file
6
web/assets/UploadPage-t63bWSjh.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/_plugin-vue_export-helper-DlAUqK2U.js
Normal file
1
web/assets/_plugin-vue_export-helper-DlAUqK2U.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
const s=(t,r)=>{const o=t.__vccOpts||t;for(const[c,e]of r)o[c]=e;return o};export{s as _};
|
||||||
7
web/assets/index-BhaIiTEj.js
Normal file
7
web/assets/index-BhaIiTEj.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/index-Catk2Spn.js
Normal file
1
web/assets/index-Catk2Spn.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{r as i,P as n,c as o,g as d}from"./Sonner.vue_vue_type_script_setup_true_lang-BwfWndxy.js";import{d as c,h as v,u as e,l,z as u,k as g,g as f}from"./index-BhaIiTEj.js";const x=c({__name:"Badge",props:{asChild:{type:Boolean},as:{},variant:{},class:{}},setup(r){const a=r,t=i(a,"class");return(s,b)=>(f(),v(e(n),l({"data-slot":"badge",class:e(o)(e(p)({variant:r.variant}),a.class)},e(t)),{default:u(()=>[g(s.$slots,"default")]),_:3},16,["class"]))}}),p=d("inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",{variants:{variant:{default:"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",secondary:"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",destructive:"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",outline:"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground"}},defaultVariants:{variant:"default"}});export{x as _};
|
||||||
1
web/assets/index-o9d4BZZI.css
Normal file
1
web/assets/index-o9d4BZZI.css
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/useForwardExpose-CRFbVhil.js
Normal file
1
web/assets/useForwardExpose-CRFbVhil.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/utils-BNRHOSl8.js
Normal file
1
web/assets/utils-BNRHOSl8.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{h as o}from"./Teleport-DV_BGdH0.js";function f(t){return t?"open":"closed"}function c(t){const e=o();for(const n of t)if(n===e||(n.focus(),o()!==e))return}export{c as f,f as g};
|
||||||
14
web/index.html
Normal file
14
web/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>vue-vite-shadcn-template</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-BhaIiTEj.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-o9d4BZZI.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
web/vite.svg
Normal file
1
web/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user