mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
refactor(storage): drop per-upload storage_driver, use storage_config as single source
Remove w_uploads.storage_driver and route all read/write/delete paths through storage.Active() backed by storage_config.driver. Block direct driver switches when uploads exist; require migration task instead. Simplify migration to cursor-based file_path iteration without per-row driver updates.
This commit is contained in:
+133
-137
@@ -3367,7 +3367,7 @@ const docTemplate = `{
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/upload.listFilesResponse"
|
"$ref": "#/definitions/handler.listFilesResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3414,7 +3414,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/upload.batchDownloadRequest"
|
"$ref": "#/definitions/handler.batchDownloadRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -3525,7 +3525,7 @@ const docTemplate = `{
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/upload.fileStatsResponse"
|
"$ref": "#/definitions/handler.fileStatsResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4654,7 +4654,7 @@ const docTemplate = `{
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/upload.listMyFilesResponse"
|
"$ref": "#/definitions/handler.listMyFilesResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4702,7 +4702,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/upload.updateMyFileRequest"
|
"$ref": "#/definitions/handler.updateMyFileRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -5688,6 +5688,134 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handler.batchDownloadRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.distributionItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.fileStatsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"categories": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.distributionItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"trend": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.trendItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.distributionItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.listFilesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.listMyFilesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.trendItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.updateMyFileRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"file_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"logger.LogEntry": {
|
"logger.LogEntry": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6280,10 +6408,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"storage_driver": {
|
|
||||||
"description": "存储引擎驱动 (如 local, s3, oss)",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
"type": {
|
||||||
"description": "业务标识类型 (如 avatar, doc, attachment)",
|
"description": "业务标识类型 (如 avatar, doc, attachment)",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -7197,134 +7321,6 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upload.batchDownloadRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"ids"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"ids": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.distributionItem": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"count": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.fileStatsResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"categories": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/upload.distributionItem"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"total_count": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total_size": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"trend": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/upload.trendItem"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"types": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/upload.distributionItem"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.listFilesResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/model.Upload"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"page_size": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.listMyFilesResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/model.Upload"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"page_size": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.trendItem": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"count": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.updateMyFileRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"access_mode": {
|
|
||||||
"type": "integer",
|
|
||||||
"enum": [
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"file_name": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 255
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user.changePasswordRequest": {
|
"user.changePasswordRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+133
-137
@@ -3360,7 +3360,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/upload.listFilesResponse"
|
"$ref": "#/definitions/handler.listFilesResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3407,7 +3407,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/upload.batchDownloadRequest"
|
"$ref": "#/definitions/handler.batchDownloadRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -3518,7 +3518,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/upload.fileStatsResponse"
|
"$ref": "#/definitions/handler.fileStatsResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4647,7 +4647,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/definitions/upload.listMyFilesResponse"
|
"$ref": "#/definitions/handler.listMyFilesResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4695,7 +4695,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/upload.updateMyFileRequest"
|
"$ref": "#/definitions/handler.updateMyFileRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -5681,6 +5681,134 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handler.batchDownloadRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.distributionItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.fileStatsResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"categories": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.distributionItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"trend": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.trendItem"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/handler.distributionItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.listFilesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.listMyFilesResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/model.Upload"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"page": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_size": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.trendItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handler.updateMyFileRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"access_mode": {
|
||||||
|
"type": "integer",
|
||||||
|
"enum": [
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"file_name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"logger.LogEntry": {
|
"logger.LogEntry": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -6273,10 +6401,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"storage_driver": {
|
|
||||||
"description": "存储引擎驱动 (如 local, s3, oss)",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": {
|
"type": {
|
||||||
"description": "业务标识类型 (如 avatar, doc, attachment)",
|
"description": "业务标识类型 (如 avatar, doc, attachment)",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -7190,134 +7314,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upload.batchDownloadRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"ids"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"ids": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.distributionItem": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"count": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.fileStatsResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"categories": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/upload.distributionItem"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"total_count": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total_size": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"trend": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/upload.trendItem"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"types": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/upload.distributionItem"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.listFilesResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/model.Upload"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"page_size": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.listMyFilesResponse": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/model.Upload"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"page_size": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.trendItem": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"count": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"date": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upload.updateMyFileRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"access_mode": {
|
|
||||||
"type": "integer",
|
|
||||||
"enum": [
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"file_name": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 255
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user.changePasswordRequest": {
|
"user.changePasswordRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
+89
-92
@@ -143,6 +143,90 @@ definitions:
|
|||||||
token:
|
token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
handler.batchDownloadRequest:
|
||||||
|
properties:
|
||||||
|
ids:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- ids
|
||||||
|
type: object
|
||||||
|
handler.distributionItem:
|
||||||
|
properties:
|
||||||
|
count:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
handler.fileStatsResponse:
|
||||||
|
properties:
|
||||||
|
categories:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/handler.distributionItem'
|
||||||
|
type: array
|
||||||
|
total_count:
|
||||||
|
type: integer
|
||||||
|
total_size:
|
||||||
|
type: integer
|
||||||
|
trend:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/handler.trendItem'
|
||||||
|
type: array
|
||||||
|
types:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/handler.distributionItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
handler.listFilesResponse:
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Upload'
|
||||||
|
type: array
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
page_size:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
handler.listMyFilesResponse:
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/model.Upload'
|
||||||
|
type: array
|
||||||
|
page:
|
||||||
|
type: integer
|
||||||
|
page_size:
|
||||||
|
type: integer
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
handler.trendItem:
|
||||||
|
properties:
|
||||||
|
count:
|
||||||
|
type: integer
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
handler.updateMyFileRequest:
|
||||||
|
properties:
|
||||||
|
access_mode:
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
type: integer
|
||||||
|
file_name:
|
||||||
|
maxLength: 255
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
logger.LogEntry:
|
logger.LogEntry:
|
||||||
properties:
|
properties:
|
||||||
data:
|
data:
|
||||||
@@ -541,9 +625,6 @@ definitions:
|
|||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/definitions/model.UploadStatus'
|
- $ref: '#/definitions/model.UploadStatus'
|
||||||
description: 状态
|
description: 状态
|
||||||
storage_driver:
|
|
||||||
description: 存储引擎驱动 (如 local, s3, oss)
|
|
||||||
type: string
|
|
||||||
type:
|
type:
|
||||||
description: 业务标识类型 (如 avatar, doc, attachment)
|
description: 业务标识类型 (如 avatar, doc, attachment)
|
||||||
type: string
|
type: string
|
||||||
@@ -1169,90 +1250,6 @@ definitions:
|
|||||||
upstream_repository:
|
upstream_repository:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
upload.batchDownloadRequest:
|
|
||||||
properties:
|
|
||||||
ids:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
minItems: 1
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- ids
|
|
||||||
type: object
|
|
||||||
upload.distributionItem:
|
|
||||||
properties:
|
|
||||||
count:
|
|
||||||
type: integer
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
size:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
upload.fileStatsResponse:
|
|
||||||
properties:
|
|
||||||
categories:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/upload.distributionItem'
|
|
||||||
type: array
|
|
||||||
total_count:
|
|
||||||
type: integer
|
|
||||||
total_size:
|
|
||||||
type: integer
|
|
||||||
trend:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/upload.trendItem'
|
|
||||||
type: array
|
|
||||||
types:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/upload.distributionItem'
|
|
||||||
type: array
|
|
||||||
type: object
|
|
||||||
upload.listFilesResponse:
|
|
||||||
properties:
|
|
||||||
items:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/model.Upload'
|
|
||||||
type: array
|
|
||||||
page:
|
|
||||||
type: integer
|
|
||||||
page_size:
|
|
||||||
type: integer
|
|
||||||
total:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
upload.listMyFilesResponse:
|
|
||||||
properties:
|
|
||||||
items:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/model.Upload'
|
|
||||||
type: array
|
|
||||||
page:
|
|
||||||
type: integer
|
|
||||||
page_size:
|
|
||||||
type: integer
|
|
||||||
total:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
upload.trendItem:
|
|
||||||
properties:
|
|
||||||
count:
|
|
||||||
type: integer
|
|
||||||
date:
|
|
||||||
type: string
|
|
||||||
size:
|
|
||||||
type: integer
|
|
||||||
type: object
|
|
||||||
upload.updateMyFileRequest:
|
|
||||||
properties:
|
|
||||||
access_mode:
|
|
||||||
enum:
|
|
||||||
- 0
|
|
||||||
- 1
|
|
||||||
type: integer
|
|
||||||
file_name:
|
|
||||||
maxLength: 255
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
user.changePasswordRequest:
|
user.changePasswordRequest:
|
||||||
properties:
|
properties:
|
||||||
new_password:
|
new_password:
|
||||||
@@ -3409,7 +3406,7 @@ paths:
|
|||||||
- $ref: '#/definitions/response.Any'
|
- $ref: '#/definitions/response.Any'
|
||||||
- properties:
|
- properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/definitions/upload.listFilesResponse'
|
$ref: '#/definitions/handler.listFilesResponse'
|
||||||
type: object
|
type: object
|
||||||
"401":
|
"401":
|
||||||
description: 未登录
|
description: 未登录
|
||||||
@@ -3501,7 +3498,7 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/upload.batchDownloadRequest'
|
$ref: '#/definitions/handler.batchDownloadRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/octet-stream
|
- application/octet-stream
|
||||||
responses:
|
responses:
|
||||||
@@ -3535,7 +3532,7 @@ paths:
|
|||||||
- $ref: '#/definitions/response.Any'
|
- $ref: '#/definitions/response.Any'
|
||||||
- properties:
|
- properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/definitions/upload.fileStatsResponse'
|
$ref: '#/definitions/handler.fileStatsResponse'
|
||||||
type: object
|
type: object
|
||||||
"401":
|
"401":
|
||||||
description: 未登录
|
description: 未登录
|
||||||
@@ -4193,7 +4190,7 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/upload.updateMyFileRequest'
|
$ref: '#/definitions/handler.updateMyFileRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -4253,7 +4250,7 @@ paths:
|
|||||||
- $ref: '#/definitions/response.Any'
|
- $ref: '#/definitions/response.Any'
|
||||||
- properties:
|
- properties:
|
||||||
data:
|
data:
|
||||||
$ref: '#/definitions/upload.listMyFilesResponse'
|
$ref: '#/definitions/handler.listMyFilesResponse'
|
||||||
type: object
|
type: object
|
||||||
"401":
|
"401":
|
||||||
description: 未登录
|
description: 未登录
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ export function FileList() {
|
|||||||
queryFn: () => services.adminUpload.listUploads(page, pageSize, debouncedKeyword || undefined),
|
queryFn: () => services.adminUpload.listUploads(page, pageSize, debouncedKeyword || undefined),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const storageDriverQuery = useQuery({
|
||||||
|
queryKey: ["admin", "storage-config", "driver"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const record = await services.adminSystemConfig.getSystemConfig("storage_config")
|
||||||
|
const cfg = JSON.parse(record.value) as {driver?: string}
|
||||||
|
return cfg.driver ?? "local"
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const files = listQuery.data?.items ?? []
|
const files = listQuery.data?.items ?? []
|
||||||
const total = listQuery.data?.total ?? 0
|
const total = listQuery.data?.total ?? 0
|
||||||
const totalPages = Math.ceil(total / pageSize)
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
@@ -470,7 +479,7 @@ export function FileList() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2 border-b border-dashed pb-2">
|
<div className="grid grid-cols-3 gap-2 border-b border-dashed pb-2">
|
||||||
<span className="text-muted-foreground font-medium">存储驱动</span>
|
<span className="text-muted-foreground font-medium">存储驱动</span>
|
||||||
<span className="col-span-2 font-mono text-foreground/90">{detailTarget.storage_driver || "local"}</span>
|
<span className="col-span-2 font-mono text-foreground/90">{storageDriverQuery.data ?? "local"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-2 border-b border-dashed pb-2">
|
<div className="grid grid-cols-3 gap-2 border-b border-dashed pb-2">
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export interface Upload {
|
|||||||
mime_type: string
|
mime_type: string
|
||||||
extension: string
|
extension: string
|
||||||
hash: string
|
hash: string
|
||||||
storage_driver: string
|
|
||||||
type: string
|
type: string
|
||||||
status: string
|
status: string
|
||||||
access_mode: number
|
access_mode: number
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ const (
|
|||||||
ConfigKeyRequired = "配置键不能为空"
|
ConfigKeyRequired = "配置键不能为空"
|
||||||
ConfigValueRequired = "配置值不能为空"
|
ConfigValueRequired = "配置值不能为空"
|
||||||
ConfigKeyExists = "配置键已存在"
|
ConfigKeyExists = "配置键已存在"
|
||||||
|
StorageDriverSwitchRequiresMigration = "存在存量文件,请通过存储迁移任务切换存储引擎"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/db"
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
@@ -88,9 +87,6 @@ func updateSystemConfig(ctx context.Context, key string, req UpdateSystemConfigR
|
|||||||
if err := tx.Model(&config).Updates(updates).Error; err != nil {
|
if err := tx.Model(&config).Updates(updates).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := repointUploadStorageDriversOnDriverSwitch(ctx, tx, key, originalDriver, req.Value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resolveStorageMigrationTasksOnDirectDriverUpdate(ctx, tx, key, originalDriver, req.Value)
|
resolveStorageMigrationTasksOnDirectDriverUpdate(ctx, tx, key, originalDriver, req.Value)
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -101,38 +97,6 @@ func updateSystemConfig(ctx context.Context, key string, req UpdateSystemConfigR
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func repointUploadStorageDriversOnDriverSwitch(
|
|
||||||
ctx context.Context,
|
|
||||||
tx *gorm.DB,
|
|
||||||
key string,
|
|
||||||
originalDriver storage.Driver,
|
|
||||||
newValue string,
|
|
||||||
) error {
|
|
||||||
if key != model.ConfigKeyStorageConfig || originalDriver == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var newCfg storage.Config
|
|
||||||
if err := json.Unmarshal([]byte(newValue), &newCfg); err != nil {
|
|
||||||
return fmt.Errorf("parse storage config for driver repoint: %w", err)
|
|
||||||
}
|
|
||||||
if newCfg.Driver == "" || newCfg.Driver == originalDriver {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := tx.Model(&model.Upload{}).
|
|
||||||
Where("storage_driver = ? AND status != ?", string(originalDriver), model.UploadStatusDeleted).
|
|
||||||
Update("storage_driver", string(newCfg.Driver))
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
if result.RowsAffected > 0 {
|
|
||||||
logger.InfoF(ctx, "[StorageConfig] switched driver %s -> %s, repointed %d upload records",
|
|
||||||
originalDriver, newCfg.Driver, result.RowsAffected)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveStorageMigrationTasksOnDirectDriverUpdate(
|
func resolveStorageMigrationTasksOnDirectDriverUpdate(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/Rain-kl/Wavelet/internal/apps/cap"
|
"github.com/Rain-kl/Wavelet/internal/apps/cap"
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/upload"
|
"github.com/Rain-kl/Wavelet/internal/apps/upload"
|
||||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||||
|
"github.com/Rain-kl/Wavelet/internal/db"
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/repository"
|
"github.com/Rain-kl/Wavelet/internal/repository"
|
||||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||||
@@ -266,11 +267,13 @@ func TestSMTP(c *gin.Context) {
|
|||||||
|
|
||||||
func isStorageConfigValidationError(err error) bool {
|
func isStorageConfigValidationError(err error) bool {
|
||||||
msg := err.Error()
|
msg := err.Error()
|
||||||
return strings.HasPrefix(msg, "解析") ||
|
return msg == StorageDriverSwitchRequiresMigration ||
|
||||||
|
strings.HasPrefix(msg, "解析") ||
|
||||||
strings.HasPrefix(msg, "验证") ||
|
strings.HasPrefix(msg, "验证") ||
|
||||||
strings.HasPrefix(msg, "初始化测试") ||
|
strings.HasPrefix(msg, "初始化测试") ||
|
||||||
strings.HasPrefix(msg, "存储连通性") ||
|
strings.HasPrefix(msg, "存储连通性") ||
|
||||||
strings.HasPrefix(msg, "序列化")
|
strings.HasPrefix(msg, "序列化") ||
|
||||||
|
strings.HasPrefix(msg, "检查存量文件")
|
||||||
}
|
}
|
||||||
|
|
||||||
func maskSensitiveConfig(key, value string) string {
|
func maskSensitiveConfig(key, value string) string {
|
||||||
@@ -322,6 +325,15 @@ func validateAndMergeStorageConfig(ctx context.Context, value string, currentCon
|
|||||||
|
|
||||||
func validateMergedStorageConfig(ctx context.Context, currentCfg, newCfg, targetCfg storage.Config) error {
|
func validateMergedStorageConfig(ctx context.Context, currentCfg, newCfg, targetCfg storage.Config) error {
|
||||||
if newCfg.Driver != "" && newCfg.Driver != currentCfg.Driver {
|
if newCfg.Driver != "" && newCfg.Driver != currentCfg.Driver {
|
||||||
|
var uploadCount int64
|
||||||
|
if err := db.DB(ctx).Model(&model.Upload{}).
|
||||||
|
Where("status != ?", model.UploadStatusDeleted).
|
||||||
|
Count(&uploadCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("检查存量文件失败: %w", err)
|
||||||
|
}
|
||||||
|
if uploadCount > 0 {
|
||||||
|
return errors.New(StorageDriverSwitchRequiresMigration)
|
||||||
|
}
|
||||||
if err := validateDriverConfig(targetCfg, newCfg.Driver); err != nil {
|
if err := validateDriverConfig(targetCfg, newCfg.Driver); err != nil {
|
||||||
return fmt.Errorf("验证目标存储配置参数失败: %w", err)
|
return fmt.Errorf("验证目标存储配置参数失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||||
@@ -441,7 +442,53 @@ func TestUpdateStorageConfigValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("reject driver switch when uploads exist", func(t *testing.T) {
|
||||||
|
upload := model.Upload{
|
||||||
|
ID: 88001,
|
||||||
|
UserID: 1,
|
||||||
|
FileName: "keep.txt",
|
||||||
|
FilePath: "uploads/keep.txt",
|
||||||
|
FileSize: 4,
|
||||||
|
MimeType: "text/plain",
|
||||||
|
Extension: "txt",
|
||||||
|
Type: "attachment",
|
||||||
|
Status: model.UploadStatusUsed,
|
||||||
|
}
|
||||||
|
if err := dbConn.Create(&upload).Error; err != nil {
|
||||||
|
t.Fatalf("seed upload failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cfg := storage.DefaultConfig()
|
||||||
|
cfg.Driver = storage.DriverS3
|
||||||
|
cfg.S3.Endpoint = "http://127.0.0.1:19998"
|
||||||
|
cfg.S3.Region = "us-east-1"
|
||||||
|
cfg.S3.Bucket = "wavelet"
|
||||||
|
cfg.S3.AccessKeyID = "test"
|
||||||
|
cfg.S3.SecretAccessKey = "test"
|
||||||
|
cfg.Local.Root = tempDir
|
||||||
|
|
||||||
|
cfgBytes, _ := json.Marshal(cfg)
|
||||||
|
payload := UpdateSystemConfigRequest{Value: string(cfgBytes)}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req, _ := http.NewRequest("PUT", "/api/v1/admin/system-configs/storage_config", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(w.Body.String(), StorageDriverSwitchRequiresMigration) {
|
||||||
|
t.Fatalf("expected migration-required error, got: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("switch to local while active s3 is unreachable", func(t *testing.T) {
|
t.Run("switch to local while active s3 is unreachable", func(t *testing.T) {
|
||||||
|
if err := dbConn.Where("1 = 1").Delete(&model.Upload{}).Error; err != nil {
|
||||||
|
t.Fatalf("clear uploads failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
activeCfg := storage.DefaultConfig()
|
activeCfg := storage.DefaultConfig()
|
||||||
activeCfg.Driver = storage.DriverS3
|
activeCfg.Driver = storage.DriverS3
|
||||||
activeCfg.S3.Endpoint = "http://127.0.0.1:9999"
|
activeCfg.S3.Endpoint = "http://127.0.0.1:9999"
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ func TestServeFileByIDAccessControl(t *testing.T) {
|
|||||||
FileSize: 5,
|
FileSize: 5,
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "avatar",
|
Type: "avatar",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
AccessMode: 1,
|
AccessMode: 1,
|
||||||
@@ -80,7 +79,6 @@ func TestServeFileByIDAccessControl(t *testing.T) {
|
|||||||
FileSize: 5,
|
FileSize: 5,
|
||||||
MimeType: "application/pdf",
|
MimeType: "application/pdf",
|
||||||
Extension: "pdf",
|
Extension: "pdf",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "attachment",
|
Type: "attachment",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
AccessMode: 1,
|
AccessMode: 1,
|
||||||
@@ -207,7 +205,6 @@ func TestImageCompression(t *testing.T) {
|
|||||||
FileSize: int64(pngBuf.Len()),
|
FileSize: int64(pngBuf.Len()),
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "avatar", // Whitelisted by default
|
Type: "avatar", // Whitelisted by default
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
AccessMode: 1,
|
AccessMode: 1,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func TestGetDistinctUploadTypes(t *testing.T) {
|
|||||||
FileSize: 10,
|
FileSize: 10,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "custom_type_xyz",
|
Type: "custom_type_xyz",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ func createInstantUpload(ctx context.Context, existing model.Upload, input insta
|
|||||||
MimeType: input.MimeType,
|
MimeType: input.MimeType,
|
||||||
Extension: input.Extension,
|
Extension: input.Extension,
|
||||||
Hash: input.FileHash,
|
Hash: input.FileHash,
|
||||||
StorageDriver: existing.StorageDriver,
|
|
||||||
Type: input.UploadType,
|
Type: input.UploadType,
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
AccessMode: input.AccessMode,
|
AccessMode: input.AccessMode,
|
||||||
@@ -142,9 +141,9 @@ func findReusableUpload(ctx context.Context, hash string, size int64) (model.Upl
|
|||||||
return repository.FindReusableUploadByHash(ctx, hash, size)
|
return repository.FindReusableUploadByHash(ctx, hash, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveNewUploadRecord(ctx context.Context, upload *model.Upload, storageDriver, filePath string) error {
|
func saveNewUploadRecord(ctx context.Context, upload *model.Upload, filePath string) error {
|
||||||
if err := repository.CreateUpload(ctx, upload); err != nil {
|
if err := repository.CreateUpload(ctx, upload); err != nil {
|
||||||
backend, backendErr := storage.ForDriver(ctx, storage.Driver(storageDriver))
|
_, backend, backendErr := storage.Active(ctx)
|
||||||
if backendErr == nil {
|
if backendErr == nil {
|
||||||
if deleteErr := backend.Delete(ctx, filePath); deleteErr != nil {
|
if deleteErr := backend.Delete(ctx, filePath); deleteErr != nil {
|
||||||
logger.WarnF(ctx, "清理未写入数据库的上传对象失败: %v", deleteErr)
|
logger.WarnF(ctx, "清理未写入数据库的上传对象失败: %v", deleteErr)
|
||||||
@@ -162,22 +161,22 @@ func loadUploadStats(ctx context.Context) ([]model.UploadStat, error) {
|
|||||||
|
|
||||||
var errUploadForbidden = errors.New("upload forbidden")
|
var errUploadForbidden = errors.New("upload forbidden")
|
||||||
|
|
||||||
func storeUploadObject(ctx context.Context, subPath string, size int64, mimeType string, buf *bytes.Buffer, meta *model.UploadMetadata) (string, string, error) {
|
func storeUploadObject(ctx context.Context, subPath string, size int64, mimeType string, buf *bytes.Buffer, meta *model.UploadMetadata) (string, error) {
|
||||||
if uploadstorage.ReadOnly(ctx) {
|
if uploadstorage.ReadOnly(ctx) {
|
||||||
return "", "", errors.New(shared.ErrStorageReadOnly)
|
return "", errors.New(shared.ErrStorageReadOnly)
|
||||||
}
|
}
|
||||||
driver, backend, err := storage.Active(ctx)
|
driver, backend, err := storage.Active(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorF(ctx, "初始化活动存储失败: %v", err)
|
logger.ErrorF(ctx, "初始化活动存储失败: %v", err)
|
||||||
return "", "", errors.New(shared.ErrSaveFileFailed)
|
return "", errors.New(shared.ErrSaveFileFailed)
|
||||||
}
|
}
|
||||||
result, err := backend.Put(ctx, subPath, bytes.NewReader(buf.Bytes()), size, mimeType)
|
result, err := backend.Put(ctx, subPath, bytes.NewReader(buf.Bytes()), size, mimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorF(ctx, "写入 %s 存储失败: %v", driver, err)
|
logger.ErrorF(ctx, "写入 %s 存储失败: %v", driver, err)
|
||||||
return "", "", errors.New(shared.ErrSaveFileFailed)
|
return "", errors.New(shared.ErrSaveFileFailed)
|
||||||
}
|
}
|
||||||
meta.Bucket = result.Bucket
|
meta.Bucket = result.Bucket
|
||||||
return string(driver), result.Key, nil
|
return result.Key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUploadAllowedExtension(ctx context.Context, ext string) string {
|
func validateUploadAllowedExtension(ctx context.Context, ext string) string {
|
||||||
|
|||||||
@@ -138,29 +138,28 @@ func UploadFile(c *gin.Context) {
|
|||||||
id := idgen.NextUint64ID()
|
id := idgen.NextUint64ID()
|
||||||
subPath := fmt.Sprintf("uploads/%s/%d.%s", time.Now().Format("2006/01/02"), id, ext)
|
subPath := fmt.Sprintf("uploads/%s/%d.%s", time.Now().Format("2006/01/02"), id, ext)
|
||||||
|
|
||||||
storageDriver, subPath, err := storeUploadObject(ctx, subPath, size, mimeType, &buf, &meta)
|
subPath, err = storeUploadObject(ctx, subPath, size, mimeType, &buf, &meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.AbortBadRequest(c, err.Error())
|
response.AbortBadRequest(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newUpload := model.Upload{
|
newUpload := model.Upload{
|
||||||
ID: id,
|
ID: id,
|
||||||
UserID: currUser.ID,
|
UserID: currUser.ID,
|
||||||
FileName: origName,
|
FileName: origName,
|
||||||
FilePath: subPath,
|
FilePath: subPath,
|
||||||
FileSize: size,
|
FileSize: size,
|
||||||
MimeType: mimeType,
|
MimeType: mimeType,
|
||||||
Extension: ext,
|
Extension: ext,
|
||||||
Hash: fileHash,
|
Hash: fileHash,
|
||||||
StorageDriver: storageDriver,
|
Type: uploadType,
|
||||||
Type: uploadType,
|
Status: model.UploadStatusUsed,
|
||||||
Status: model.UploadStatusUsed,
|
AccessMode: accessMode,
|
||||||
AccessMode: accessMode,
|
Metadata: meta,
|
||||||
Metadata: meta,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := saveNewUploadRecord(ctx, &newUpload, storageDriver, subPath); err != nil {
|
if err := saveNewUploadRecord(ctx, &newUpload, subPath); err != nil {
|
||||||
response.AbortBadRequest(c, shared.ErrSaveUploadRecordFailed)
|
response.AbortBadRequest(c, shared.ErrSaveUploadRecordFailed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,10 +193,6 @@ func TestUploadFile(t *testing.T) {
|
|||||||
t.Errorf("incorrect mime type detected: %s", dbRecord.MimeType)
|
t.Errorf("incorrect mime type detected: %s", dbRecord.MimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbRecord.StorageDriver != "s3" {
|
|
||||||
t.Errorf("expected storage driver s3, got %s", dbRecord.StorageDriver)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dbRecord.Metadata.Extra["source"] != "test_runner" {
|
if dbRecord.Metadata.Extra["source"] != "test_runner" {
|
||||||
t.Errorf("expected extra meta 'source' to be 'test_runner', got %v", dbRecord.Metadata.Extra)
|
t.Errorf("expected extra meta 'source' to be 'test_runner', got %v", dbRecord.Metadata.Extra)
|
||||||
}
|
}
|
||||||
@@ -325,10 +321,6 @@ func TestUploadFile(t *testing.T) {
|
|||||||
t.Fatalf("failed to unmarshal local upload record: %v", err)
|
t.Fatalf("failed to unmarshal local upload record: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if localRecord.StorageDriver != "local" {
|
|
||||||
t.Errorf("expected storage driver local, got %s", localRecord.StorageDriver)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm file was actually written to local disk
|
// Confirm file was actually written to local disk
|
||||||
fileContent, err := os.ReadFile(localRecord.FilePath)
|
fileContent, err := os.ReadFile(localRecord.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -358,7 +350,6 @@ func TestDownloadFile(t *testing.T) {
|
|||||||
FileSize: 12,
|
FileSize: 12,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +417,6 @@ func TestListFiles(t *testing.T) {
|
|||||||
FileSize: 10,
|
FileSize: 10,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -437,7 +427,6 @@ func TestListFiles(t *testing.T) {
|
|||||||
FileSize: 20,
|
FileSize: 20,
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -448,7 +437,6 @@ func TestListFiles(t *testing.T) {
|
|||||||
FileSize: 30,
|
FileSize: 30,
|
||||||
MimeType: "text/markdown",
|
MimeType: "text/markdown",
|
||||||
Extension: "md",
|
Extension: "md",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -459,7 +447,6 @@ func TestListFiles(t *testing.T) {
|
|||||||
FileSize: 40,
|
FileSize: 40,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -582,7 +569,6 @@ func TestBatchDownloadFiles(t *testing.T) {
|
|||||||
FileSize: 13,
|
FileSize: 13,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -593,7 +579,6 @@ func TestBatchDownloadFiles(t *testing.T) {
|
|||||||
FileSize: 13,
|
FileSize: 13,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -604,7 +589,6 @@ func TestBatchDownloadFiles(t *testing.T) {
|
|||||||
FileSize: 28,
|
FileSize: 28,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -773,7 +757,6 @@ func TestGetFileStats(t *testing.T) {
|
|||||||
FileSize: 100,
|
FileSize: 100,
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "generic",
|
Type: "generic",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
@@ -786,7 +769,6 @@ func TestGetFileStats(t *testing.T) {
|
|||||||
FileSize: 500,
|
FileSize: 500,
|
||||||
MimeType: "video/mp4",
|
MimeType: "video/mp4",
|
||||||
Extension: "mp4",
|
Extension: "mp4",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "generic",
|
Type: "generic",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
CreatedAt: time.Now().AddDate(0, 0, -2), // 2 days ago
|
CreatedAt: time.Now().AddDate(0, 0, -2), // 2 days ago
|
||||||
@@ -799,7 +781,6 @@ func TestGetFileStats(t *testing.T) {
|
|||||||
FileSize: 200,
|
FileSize: 200,
|
||||||
MimeType: "application/pdf",
|
MimeType: "application/pdf",
|
||||||
Extension: "pdf",
|
Extension: "pdf",
|
||||||
StorageDriver: "local",
|
|
||||||
Type: "avatar", // different type
|
Type: "avatar", // different type
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
CreatedAt: time.Now().AddDate(0, 0, -10), // older than 7 days
|
CreatedAt: time.Now().AddDate(0, 0, -10), // older than 7 days
|
||||||
@@ -891,7 +872,6 @@ func TestUserUploadManagement(t *testing.T) {
|
|||||||
FileSize: 100,
|
FileSize: 100,
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
@@ -903,7 +883,6 @@ func TestUserUploadManagement(t *testing.T) {
|
|||||||
FileSize: 200,
|
FileSize: 200,
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: "local",
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
package shared
|
package shared
|
||||||
|
|
||||||
import "github.com/Rain-kl/Wavelet/internal/storage"
|
|
||||||
|
|
||||||
// Upload size, path, media quality, and cache constants shared across subpackages.
|
// Upload size, path, media quality, and cache constants shared across subpackages.
|
||||||
const (
|
const (
|
||||||
MaxUploadSize = 32 * 1024 * 1024 // 32MB
|
MaxUploadSize = 32 * 1024 * 1024 // 32MB
|
||||||
@@ -15,7 +13,6 @@ const (
|
|||||||
ImageQualityMedium = "medium"
|
ImageQualityMedium = "medium"
|
||||||
ImageQualityHigh = "high"
|
ImageQualityHigh = "high"
|
||||||
ImageQualityOrigin = "origin"
|
ImageQualityOrigin = "origin"
|
||||||
StorageDriverLocal = string(storage.DriverLocal)
|
|
||||||
DefaultPublicUploadType = "avatar"
|
DefaultPublicUploadType = "avatar"
|
||||||
FileStatsTrendDays = 7
|
FileStatsTrendDays = 7
|
||||||
MaxS3KeyLength = 1024
|
MaxS3KeyLength = 1024
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/Rain-kl/Wavelet/internal/model"
|
"github.com/Rain-kl/Wavelet/internal/model"
|
||||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||||
@@ -22,46 +21,11 @@ func ReadOnly(ctx context.Context) bool {
|
|||||||
return state.ReadOnly
|
return state.ReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenStoredObject opens a stored upload object from its configured backend.
|
// OpenStoredObject opens a stored upload object from the active storage backend.
|
||||||
func OpenStoredObject(ctx context.Context, upload *model.Upload) (*storage.Object, error) {
|
func OpenStoredObject(ctx context.Context, upload *model.Upload) (*storage.Object, error) {
|
||||||
driver := storage.Driver(upload.StorageDriver)
|
_, backend, err := storage.Active(ctx)
|
||||||
if driver == "" {
|
|
||||||
driver = storage.DriverLocal
|
|
||||||
}
|
|
||||||
backend, err := backendForStoredDriver(ctx, driver)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return backend.Get(ctx, upload.FilePath)
|
return backend.Get(ctx, upload.FilePath)
|
||||||
}
|
|
||||||
|
|
||||||
func backendForStoredDriver(ctx context.Context, driver storage.Driver) (storage.Backend, error) {
|
|
||||||
backend, err := storage.ForDriver(ctx, driver)
|
|
||||||
if err == nil {
|
|
||||||
return backend, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
target, ok, targetErr := CurrentMigrationTargetConfig(ctx)
|
|
||||||
if targetErr != nil {
|
|
||||||
return nil, targetErr
|
|
||||||
}
|
|
||||||
if ok && target.Driver == driver {
|
|
||||||
return storage.NewBackend(ctx, target, driver)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("storage configuration for driver %q is unavailable", driver)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentMigrationTargetConfig returns the pending migration target config when available.
|
|
||||||
func CurrentMigrationTargetConfig(ctx context.Context) (storage.Config, bool, error) {
|
|
||||||
state := LoadMigrationAccessState(ctx)
|
|
||||||
if state.LoadErr != nil {
|
|
||||||
return storage.Config{}, false, state.LoadErr
|
|
||||||
}
|
|
||||||
if state.TargetErr != nil {
|
|
||||||
return storage.Config{}, false, state.TargetErr
|
|
||||||
}
|
|
||||||
if !state.HasTarget {
|
|
||||||
return storage.Config{}, false, nil
|
|
||||||
}
|
|
||||||
return state.Target, true, nil
|
|
||||||
}
|
}
|
||||||
@@ -84,11 +84,7 @@ func (h *SystemCleanupHandler) Execute(ctx context.Context, _ []byte) (*task.Tas
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
driver := storage.Driver(u.StorageDriver)
|
_, backend, err := storage.Active(ctx)
|
||||||
if driver == "" {
|
|
||||||
driver = storage.DriverLocal
|
|
||||||
}
|
|
||||||
backend, err := storage.ForDriver(ctx, driver)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ const (
|
|||||||
StorageMigrationTask = uploadstorage.StorageMigrationTask
|
StorageMigrationTask = uploadstorage.StorageMigrationTask
|
||||||
// TaskTypeStorageMigration is the task metadata type for storage migration.
|
// TaskTypeStorageMigration is the task metadata type for storage migration.
|
||||||
TaskTypeStorageMigration = "storage_migration"
|
TaskTypeStorageMigration = "storage_migration"
|
||||||
|
|
||||||
colStorageDriver = "storage_driver"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageMigrationMeta describes the manually dispatchable migration task.
|
// StorageMigrationMeta describes the manually dispatchable migration task.
|
||||||
@@ -136,7 +134,7 @@ func (h *MigrationHandler) Execute(ctx context.Context, payload []byte) (*task.T
|
|||||||
return &task.TaskResult{Message: message}, nil
|
return &task.TaskResult{Message: message}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
total, err := countStorageObjects(ctx, active.Driver)
|
total, err := countStorageObjects(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("count source objects: %w", err)
|
return nil, fmt.Errorf("count source objects: %w", err)
|
||||||
}
|
}
|
||||||
@@ -159,7 +157,7 @@ func (h *MigrationHandler) Execute(ctx context.Context, payload []byte) (*task.T
|
|||||||
}
|
}
|
||||||
|
|
||||||
task.AppendLog(ctx, "开始存储迁移: %s -> %s,总对象数: %d", active.Driver, target.Driver, total)
|
task.AppendLog(ctx, "开始存储迁移: %s -> %s,总对象数: %d", active.Driver, target.Driver, total)
|
||||||
migrated, err := migrateObjects(ctx, sourceBackend, targetBackend, active.Driver, target.Driver, total)
|
migrated, err := migrateObjects(ctx, sourceBackend, targetBackend, total)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -172,10 +170,10 @@ func (h *MigrationHandler) Execute(ctx context.Context, payload []byte) (*task.T
|
|||||||
return &task.TaskResult{Message: message}, nil
|
return &task.TaskResult{Message: message}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func countStorageObjects(ctx context.Context, driver storage.Driver) (int64, error) {
|
func countStorageObjects(ctx context.Context) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := db.DB(ctx).Model(&model.Upload{}).
|
err := db.DB(ctx).Model(&model.Upload{}).
|
||||||
Where("storage_driver = ? AND status != ?", driver, model.UploadStatusDeleted).
|
Where("status != ?", model.UploadStatusDeleted).
|
||||||
Distinct("file_path").
|
Distinct("file_path").
|
||||||
Count(&count).Error
|
Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
@@ -189,18 +187,24 @@ func hasUnresolvedMigrationTask(ctx context.Context) (bool, error) {
|
|||||||
return execution.Status == model.TaskExecutionStatusPending || execution.Status == model.TaskExecutionStatusRunning, nil
|
return execution.Status == model.TaskExecutionStatusPending || execution.Status == model.TaskExecutionStatusRunning, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type migrationObject struct {
|
||||||
|
FilePath string `gorm:"column:file_path"`
|
||||||
|
FileSize int64 `gorm:"column:file_size"`
|
||||||
|
MimeType string `gorm:"column:mime_type"`
|
||||||
|
Hash string `gorm:"column:hash"`
|
||||||
|
}
|
||||||
|
|
||||||
func migrateObjects(
|
func migrateObjects(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sourceBackend storage.Backend,
|
sourceBackend storage.Backend,
|
||||||
targetBackend storage.Backend,
|
targetBackend storage.Backend,
|
||||||
sourceDriver storage.Driver,
|
|
||||||
targetDriver storage.Driver,
|
|
||||||
total int64,
|
total int64,
|
||||||
) (int64, error) {
|
) (int64, error) {
|
||||||
const batchSize = 50
|
const batchSize = 50
|
||||||
const migrationConcurrency = 10
|
const migrationConcurrency = 10
|
||||||
const sha256HexLength = 64
|
const sha256HexLength = 64
|
||||||
var migrated int64
|
var migrated int64
|
||||||
|
var lastFilePath string
|
||||||
for {
|
for {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return atomic.LoadInt64(&migrated), fmt.Errorf("storage migration canceled: %w", err)
|
return atomic.LoadInt64(&migrated), fmt.Errorf("storage migration canceled: %w", err)
|
||||||
@@ -208,16 +212,14 @@ func migrateObjects(
|
|||||||
|
|
||||||
task.AppendLog(ctx, "正在查询待迁移对象批次,当前已完成迁移: %d/%d", atomic.LoadInt64(&migrated), total)
|
task.AppendLog(ctx, "正在查询待迁移对象批次,当前已完成迁移: %d/%d", atomic.LoadInt64(&migrated), total)
|
||||||
|
|
||||||
var objects []struct {
|
var objects []migrationObject
|
||||||
FilePath string `gorm:"column:file_path"`
|
query := db.DB(ctx).Model(&model.Upload{}).
|
||||||
FileSize int64 `gorm:"column:file_size"`
|
|
||||||
MimeType string `gorm:"column:mime_type"`
|
|
||||||
Hash string `gorm:"column:hash"`
|
|
||||||
}
|
|
||||||
if err := db.DB(ctx).Model(&model.Upload{}).
|
|
||||||
Select("file_path, MAX(file_size) AS file_size, MAX(mime_type) AS mime_type, MAX(hash) AS hash").
|
Select("file_path, MAX(file_size) AS file_size, MAX(mime_type) AS mime_type, MAX(hash) AS hash").
|
||||||
Where("storage_driver = ? AND status != ?", sourceDriver, model.UploadStatusDeleted).
|
Where("status != ?", model.UploadStatusDeleted)
|
||||||
Group("file_path").
|
if lastFilePath != "" {
|
||||||
|
query = query.Where("file_path > ?", lastFilePath)
|
||||||
|
}
|
||||||
|
if err := query.Group("file_path").
|
||||||
Order("file_path ASC").
|
Order("file_path ASC").
|
||||||
Limit(batchSize).
|
Limit(batchSize).
|
||||||
Scan(&objects).Error; err != nil {
|
Scan(&objects).Error; err != nil {
|
||||||
@@ -228,6 +230,7 @@ func migrateObjects(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastFilePath = objects[len(objects)-1].FilePath
|
||||||
task.AppendLog(ctx, "获取当前批次迁移对象,批次大小: %d,实际获取对象数: %d", batchSize, len(objects))
|
task.AppendLog(ctx, "获取当前批次迁移对象,批次大小: %d,实际获取对象数: %d", batchSize, len(objects))
|
||||||
|
|
||||||
var g errgroup.Group
|
var g errgroup.Group
|
||||||
@@ -236,7 +239,7 @@ func migrateObjects(
|
|||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
obj := object
|
obj := object
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
if err := migrateSingleObject(ctx, sourceBackend, targetBackend, sourceDriver, targetDriver, obj, sha256HexLength); err != nil {
|
if err := migrateSingleObject(ctx, sourceBackend, targetBackend, obj, sha256HexLength); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
atomic.AddInt64(&migrated, 1)
|
atomic.AddInt64(&migrated, 1)
|
||||||
@@ -257,25 +260,11 @@ func migrateSingleObject(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sourceBackend storage.Backend,
|
sourceBackend storage.Backend,
|
||||||
targetBackend storage.Backend,
|
targetBackend storage.Backend,
|
||||||
sourceDriver storage.Driver,
|
obj migrationObject,
|
||||||
targetDriver storage.Driver,
|
|
||||||
obj struct {
|
|
||||||
FilePath string `gorm:"column:file_path"`
|
|
||||||
FileSize int64 `gorm:"column:file_size"`
|
|
||||||
MimeType string `gorm:"column:mime_type"`
|
|
||||||
Hash string `gorm:"column:hash"`
|
|
||||||
},
|
|
||||||
sha256HexLength int,
|
sha256HexLength int,
|
||||||
) error {
|
) error {
|
||||||
if shouldSkipMigration(ctx, targetBackend, obj) {
|
if shouldSkipMigration(ctx, targetBackend, obj) {
|
||||||
task.AppendLog(ctx, "[跳过迁移] 目标存储已存在相同文件且校验一致: %s", obj.FilePath)
|
task.AppendLog(ctx, "[跳过迁移] 目标存储已存在相同文件: %s", obj.FilePath)
|
||||||
if err := db.DB(ctx).Model(&model.Upload{}).
|
|
||||||
Where("storage_driver = ? AND file_path = ?", sourceDriver, obj.FilePath).
|
|
||||||
Updates(map[string]any{
|
|
||||||
colStorageDriver: targetDriver,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return fmt.Errorf("update migrated object %q: %w", obj.FilePath, err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +272,7 @@ func migrateSingleObject(
|
|||||||
source, err := sourceBackend.Get(ctx, obj.FilePath)
|
source, err := sourceBackend.Get(ctx, obj.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isNotFoundError(err) {
|
if isNotFoundError(err) {
|
||||||
return markMissingMigrationObjectDeleted(ctx, sourceDriver, targetDriver, obj.FilePath, err)
|
return markMissingMigrationObjectDeleted(ctx, obj.FilePath, err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("open source object %q: %w", obj.FilePath, err)
|
return fmt.Errorf("open source object %q: %w", obj.FilePath, err)
|
||||||
}
|
}
|
||||||
@@ -319,28 +308,22 @@ func migrateSingleObject(
|
|||||||
task.AppendLog(ctx, "[校验通过] 文件一致性校验成功: %s", targetResult.Key)
|
task.AppendLog(ctx, "[校验通过] 文件一致性校验成功: %s", targetResult.Key)
|
||||||
}
|
}
|
||||||
|
|
||||||
task.AppendLog(ctx, "[更新数据库] 正在更新文件 %s 的存储路径与驱动信息为: %s (%s)", obj.FilePath, targetResult.Key, targetDriver)
|
if targetResult.Key != obj.FilePath {
|
||||||
if err := db.DB(ctx).Model(&model.Upload{}).
|
task.AppendLog(ctx, "[更新数据库] 正在更新文件路径: %s -> %s", obj.FilePath, targetResult.Key)
|
||||||
Where("storage_driver = ? AND file_path = ?", sourceDriver, obj.FilePath).
|
if err := db.DB(ctx).Model(&model.Upload{}).
|
||||||
Updates(map[string]any{
|
Where("file_path = ? AND status != ?", obj.FilePath, model.UploadStatusDeleted).
|
||||||
colStorageDriver: targetDriver,
|
Update("file_path", targetResult.Key).Error; err != nil {
|
||||||
"file_path": targetResult.Key,
|
return fmt.Errorf("update migrated object %q: %w", obj.FilePath, err)
|
||||||
}).Error; err != nil {
|
}
|
||||||
return fmt.Errorf("update migrated object %q: %w", obj.FilePath, err)
|
|
||||||
}
|
}
|
||||||
task.AppendLog(ctx, "[迁移成功] 文件已完成迁移: %s -> %s", obj.FilePath, targetResult.Key)
|
task.AppendLog(ctx, "[迁移成功] 文件已完成迁移: %s", targetResult.Key)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldSkipMigration(
|
func shouldSkipMigration(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetBackend storage.Backend,
|
targetBackend storage.Backend,
|
||||||
obj struct {
|
obj migrationObject,
|
||||||
FilePath string `gorm:"column:file_path"`
|
|
||||||
FileSize int64 `gorm:"column:file_size"`
|
|
||||||
MimeType string `gorm:"column:mime_type"`
|
|
||||||
Hash string `gorm:"column:hash"`
|
|
||||||
},
|
|
||||||
) bool {
|
) bool {
|
||||||
targetObj, err := targetBackend.Get(ctx, obj.FilePath)
|
targetObj, err := targetBackend.Get(ctx, obj.FilePath)
|
||||||
if err != nil || targetObj == nil || targetObj.Body == nil {
|
if err != nil || targetObj == nil || targetObj.Body == nil {
|
||||||
@@ -355,8 +338,6 @@ func shouldSkipMigration(
|
|||||||
|
|
||||||
func markMissingMigrationObjectDeleted(
|
func markMissingMigrationObjectDeleted(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sourceDriver storage.Driver,
|
|
||||||
targetDriver storage.Driver,
|
|
||||||
filePath string,
|
filePath string,
|
||||||
sourceErr error,
|
sourceErr error,
|
||||||
) error {
|
) error {
|
||||||
@@ -364,16 +345,13 @@ func markMissingMigrationObjectDeleted(
|
|||||||
|
|
||||||
var affectedUploads []model.Upload
|
var affectedUploads []model.Upload
|
||||||
if err := db.DB(ctx).
|
if err := db.DB(ctx).
|
||||||
Where("storage_driver = ? AND file_path = ? AND status != ?", sourceDriver, filePath, model.UploadStatusDeleted).
|
Where("file_path = ? AND status != ?", filePath, model.UploadStatusDeleted).
|
||||||
Find(&affectedUploads).Error; err != nil {
|
Find(&affectedUploads).Error; err != nil {
|
||||||
return fmt.Errorf("load missing object uploads %q: %w", filePath, err)
|
return fmt.Errorf("load missing object uploads %q: %w", filePath, err)
|
||||||
}
|
}
|
||||||
if err := db.DB(ctx).Model(&model.Upload{}).
|
if err := db.DB(ctx).Model(&model.Upload{}).
|
||||||
Where("storage_driver = ? AND file_path = ?", sourceDriver, filePath).
|
Where("file_path = ?", filePath).
|
||||||
Updates(map[string]any{
|
Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||||
"status": model.UploadStatusDeleted,
|
|
||||||
colStorageDriver: targetDriver,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return fmt.Errorf("update missing object %q: %w", filePath, err)
|
return fmt.Errorf("update missing object %q: %w", filePath, err)
|
||||||
}
|
}
|
||||||
for i := range affectedUploads {
|
for i := range affectedUploads {
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ func TestMigrationHandlerExecute(t *testing.T) {
|
|||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
Hash: "hash",
|
Hash: "hash",
|
||||||
StorageDriver: string(storage.DriverLocal),
|
|
||||||
Type: "attachment",
|
Type: "attachment",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
}
|
}
|
||||||
@@ -106,9 +105,6 @@ func TestMigrationHandlerExecute(t *testing.T) {
|
|||||||
if err := dbConn.First(&migrated, upload.ID).Error; err != nil {
|
if err := dbConn.First(&migrated, upload.ID).Error; err != nil {
|
||||||
t.Fatalf("First(upload) returned error: %v", err)
|
t.Fatalf("First(upload) returned error: %v", err)
|
||||||
}
|
}
|
||||||
if migrated.StorageDriver != string(storage.DriverS3) {
|
|
||||||
t.Errorf("StorageDriver = %q, want %q", migrated.StorageDriver, storage.DriverS3)
|
|
||||||
}
|
|
||||||
current, err := storage.LoadConfig(ctx)
|
current, err := storage.LoadConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LoadConfig() returned error: %v", err)
|
t.Fatalf("LoadConfig() returned error: %v", err)
|
||||||
@@ -169,7 +165,6 @@ func TestMigrationHandlerExecuteWithHashValidation(t *testing.T) {
|
|||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Invalid hash
|
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Invalid hash
|
||||||
StorageDriver: string(storage.DriverLocal),
|
|
||||||
Type: "attachment",
|
Type: "attachment",
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
}
|
}
|
||||||
@@ -224,8 +219,8 @@ func TestMigrationHandlerExecuteWithHashValidation(t *testing.T) {
|
|||||||
if err := dbConn.First(&migrated, uploadIncorrect.ID).Error; err != nil {
|
if err := dbConn.First(&migrated, uploadIncorrect.ID).Error; err != nil {
|
||||||
t.Fatalf("First(upload) returned error: %v", err)
|
t.Fatalf("First(upload) returned error: %v", err)
|
||||||
}
|
}
|
||||||
if migrated.StorageDriver != string(storage.DriverS3) {
|
if migrated.FilePath != "uploads/test-hash.txt" {
|
||||||
t.Errorf("StorageDriver = %q, want %q", migrated.StorageDriver, storage.DriverS3)
|
t.Errorf("FilePath = %q, want %q", migrated.FilePath, "uploads/test-hash.txt")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ func TestSystemCleanupHandler_Execute(t *testing.T) {
|
|||||||
func(ctx context.Context, key string) error { return nil },
|
func(ctx context.Context, key string) error { return nil },
|
||||||
)
|
)
|
||||||
defer storageMock()
|
defer storageMock()
|
||||||
|
storage.IsEnabledFunc = func() bool { return true }
|
||||||
|
defer func() { storage.IsEnabledFunc = func() bool { return false } }()
|
||||||
|
storage.ResetCache()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := db.DB(ctx).AutoMigrate(&model.PushHistory{})
|
err := db.DB(ctx).AutoMigrate(&model.PushHistory{})
|
||||||
@@ -56,27 +59,27 @@ func TestSystemCleanupHandler_Execute(t *testing.T) {
|
|||||||
{
|
{
|
||||||
UserID: 1001, FileName: "old_file_1.jpg", FilePath: "uploads/old_1.jpg",
|
UserID: 1001, FileName: "old_file_1.jpg", FilePath: "uploads/old_1.jpg",
|
||||||
FileSize: 1024, MimeType: "image/jpeg", Extension: "jpg", Hash: "hash1",
|
FileSize: 1024, MimeType: "image/jpeg", Extension: "jpg", Hash: "hash1",
|
||||||
StorageDriver: "s3", Type: "attachment", Status: model.UploadStatusPending,
|
Type: "attachment", Status: model.UploadStatusPending,
|
||||||
CreatedAt: twoHoursAgo,
|
CreatedAt: twoHoursAgo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
UserID: 1001, FileName: "old_file_2.png", FilePath: "uploads/old_2.png",
|
UserID: 1001, FileName: "old_file_2.png", FilePath: "uploads/old_2.png",
|
||||||
FileSize: 2048, MimeType: "image/png", Extension: "png", Hash: "hash2",
|
FileSize: 2048, MimeType: "image/png", Extension: "png", Hash: "hash2",
|
||||||
StorageDriver: "s3", Type: "attachment", Status: model.UploadStatusPending,
|
Type: "attachment", Status: model.UploadStatusPending,
|
||||||
CreatedAt: twoHoursAgo,
|
CreatedAt: twoHoursAgo,
|
||||||
},
|
},
|
||||||
// 状态为 used 的记录 —— 不应被清理
|
// 状态为 used 的记录 —— 不应被清理
|
||||||
{
|
{
|
||||||
UserID: 1001, FileName: "used_file.jpg", FilePath: "uploads/used.jpg",
|
UserID: 1001, FileName: "used_file.jpg", FilePath: "uploads/used.jpg",
|
||||||
FileSize: 512, MimeType: "image/jpeg", Extension: "jpg", Hash: "hash3",
|
FileSize: 512, MimeType: "image/jpeg", Extension: "jpg", Hash: "hash3",
|
||||||
StorageDriver: "s3", Type: "attachment", Status: model.UploadStatusUsed,
|
Type: "attachment", Status: model.UploadStatusUsed,
|
||||||
CreatedAt: twoHoursAgo,
|
CreatedAt: twoHoursAgo,
|
||||||
},
|
},
|
||||||
// 不到1小时的 pending 记录 —— 不应被清理
|
// 不到1小时的 pending 记录 —— 不应被清理
|
||||||
{
|
{
|
||||||
UserID: 1001, FileName: "recent_file.jpg", FilePath: "uploads/recent.jpg",
|
UserID: 1001, FileName: "recent_file.jpg", FilePath: "uploads/recent.jpg",
|
||||||
FileSize: 256, MimeType: "image/jpeg", Extension: "jpg", Hash: "hash4",
|
FileSize: 256, MimeType: "image/jpeg", Extension: "jpg", Hash: "hash4",
|
||||||
StorageDriver: "s3", Type: "attachment", Status: model.UploadStatusPending,
|
Type: "attachment", Status: model.UploadStatusPending,
|
||||||
CreatedAt: now.Add(-10 * time.Minute),
|
CreatedAt: now.Add(-10 * time.Minute),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -283,7 +286,6 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) {
|
|||||||
FilePath: firstPath,
|
FilePath: firstPath,
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: shared.StorageDriverLocal,
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -293,7 +295,6 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) {
|
|||||||
FilePath: secondPath,
|
FilePath: secondPath,
|
||||||
MimeType: "application/octet-stream",
|
MimeType: "application/octet-stream",
|
||||||
Extension: "jpg",
|
Extension: "jpg",
|
||||||
StorageDriver: shared.StorageDriverLocal,
|
|
||||||
Status: model.UploadStatusPending,
|
Status: model.UploadStatusPending,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -303,7 +304,6 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) {
|
|||||||
FilePath: filepath.Join(testDir, "notes.txt"),
|
FilePath: filepath.Join(testDir, "notes.txt"),
|
||||||
MimeType: "text/plain",
|
MimeType: "text/plain",
|
||||||
Extension: "txt",
|
Extension: "txt",
|
||||||
StorageDriver: shared.StorageDriverLocal,
|
|
||||||
Status: model.UploadStatusUsed,
|
Status: model.UploadStatusUsed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -313,7 +313,6 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) {
|
|||||||
FilePath: firstPath,
|
FilePath: firstPath,
|
||||||
MimeType: "image/png",
|
MimeType: "image/png",
|
||||||
Extension: "png",
|
Extension: "png",
|
||||||
StorageDriver: shared.StorageDriverLocal,
|
|
||||||
Status: model.UploadStatusDeleted,
|
Status: model.UploadStatusDeleted,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- +goose Up
|
||||||
|
DROP INDEX IF EXISTS idx_w_uploads_storage_driver_status;
|
||||||
|
ALTER TABLE w_uploads DROP COLUMN IF EXISTS storage_driver;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE w_uploads ADD COLUMN IF NOT EXISTS storage_driver VARCHAR(50) NOT NULL DEFAULT 'local';
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_storage_driver_status ON w_uploads (storage_driver, status);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- +goose Up
|
||||||
|
DROP INDEX IF EXISTS idx_w_uploads_storage_driver_status;
|
||||||
|
-- SQLite lacks DROP COLUMN IF EXISTS; goose runs this only after initial schema created the column.
|
||||||
|
ALTER TABLE w_uploads DROP COLUMN storage_driver;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE w_uploads ADD COLUMN storage_driver VARCHAR(50) NOT NULL DEFAULT 'local';
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_w_uploads_storage_driver_status ON w_uploads (storage_driver, status);
|
||||||
@@ -40,7 +40,6 @@ type Upload struct {
|
|||||||
MimeType string `json:"mime_type" gorm:"size:100;not null"` // 媒体类型 (MIME, 如 image/png)
|
MimeType string `json:"mime_type" gorm:"size:100;not null"` // 媒体类型 (MIME, 如 image/png)
|
||||||
Extension string `json:"extension" gorm:"size:50;not null"` // 文件后缀名 (不含点,如 png, pdf)
|
Extension string `json:"extension" gorm:"size:50;not null"` // 文件后缀名 (不含点,如 png, pdf)
|
||||||
Hash string `json:"hash" gorm:"size:64;index"` // 文件哈希 (SHA-256/MD5,可用于排重)
|
Hash string `json:"hash" gorm:"size:64;index"` // 文件哈希 (SHA-256/MD5,可用于排重)
|
||||||
StorageDriver string `json:"storage_driver" gorm:"size:50;not null"` // 存储引擎驱动 (如 local, s3, oss)
|
|
||||||
Type string `json:"type" gorm:"column:type;size:50;not null;index"` // 业务标识类型 (如 avatar, doc, attachment)
|
Type string `json:"type" gorm:"column:type;size:50;not null;index"` // 业务标识类型 (如 avatar, doc, attachment)
|
||||||
Status UploadStatus `json:"status" gorm:"type:varchar(20);not null"` // 状态
|
Status UploadStatus `json:"status" gorm:"type:varchar(20);not null"` // 状态
|
||||||
AccessMode int `json:"access_mode" gorm:"column:access_mode;not null;default:0"`
|
AccessMode int `json:"access_mode" gorm:"column:access_mode;not null;default:0"`
|
||||||
|
|||||||
@@ -155,26 +155,6 @@ func Active(ctx context.Context) (Driver, Backend, error) {
|
|||||||
return activeDriver, activeBackend, nil
|
return activeDriver, activeBackend, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForDriver returns the active or pending backend for an upload record, reusing the active singleton if matched.
|
|
||||||
func ForDriver(ctx context.Context, driver Driver) (Backend, error) {
|
|
||||||
if driver == DriverS3 && mockBackend != nil {
|
|
||||||
return mockBackend, nil
|
|
||||||
}
|
|
||||||
activeDrv, activeBnd, err := Active(ctx)
|
|
||||||
if err == nil && activeDrv == driver {
|
|
||||||
return activeBnd, nil
|
|
||||||
}
|
|
||||||
cfg, err := LoadConfig(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
backend, err := NewBackend(ctx, cfg, driver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("storage configuration for driver %q is unavailable: %w", driver, err)
|
|
||||||
}
|
|
||||||
return backend, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type functionBackend struct {
|
type functionBackend struct {
|
||||||
put func(context.Context, string, io.Reader, int64, string) error
|
put func(context.Context, string, io.Reader, int64, string) error
|
||||||
get func(context.Context, string) (*Object, error)
|
get func(context.Context, string) (*Object, error)
|
||||||
|
|||||||
@@ -70,16 +70,7 @@ func TestStorageCache(t *testing.T) {
|
|||||||
t.Errorf("Active returned driver %v, backend %v; expected %v, %v", drv, bnd, DriverLocal, mockBnd)
|
t.Errorf("Active returned driver %v, backend %v; expected %v, %v", drv, bnd, DriverLocal, mockBnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Test ForDriver returns cached backend
|
// 5. Test ResetCache again
|
||||||
bnd2, err := ForDriver(ctx, DriverLocal)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ForDriver failed: %v", err)
|
|
||||||
}
|
|
||||||
if bnd2 != mockBnd {
|
|
||||||
t.Errorf("ForDriver returned backend %v, expected %v", bnd2, mockBnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Test ResetCache again
|
|
||||||
ResetCache()
|
ResetCache()
|
||||||
if activeConfigJSON != "" || activeDriver != "" || activeBackend != nil || !lastChecked.IsZero() {
|
if activeConfigJSON != "" || activeDriver != "" || activeBackend != nil || !lastChecked.IsZero() {
|
||||||
t.Fatal("ResetCache did not clear cache variables after setting them")
|
t.Fatal("ResetCache did not clear cache variables after setting them")
|
||||||
|
|||||||
Reference in New Issue
Block a user