Skip to content

Commit fdada80

Browse files
committed
wip: 切片上传、文件秒传、断点续传陆续适配中当前进度(本地存储已适配)
1 parent 1733111 commit fdada80

30 files changed

+622
-178
lines changed

.github/workflows/docker-image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
context: .
4242
platforms: linux/amd64,linux/arm64
4343
push: true
44-
tags: ${{ secrets.DOCKER_USERNAME }}/filecodebox:latest
44+
tags: ${{ secrets.DOCKER_USERNAME }}/filecodebox:beta
4545
cache-from: type=local,src=/tmp/.buildx-cache
4646
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
4747
- name: Move cache

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,6 @@ for_test.py
154154
data/.env
155155
.backup/
156156
/cloc-1.64.exe
157+
158+
# Ignore node_modules
159+
node_modules/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from tortoise import connections
2+
3+
4+
async def create_upload_chunk_and_update_file_codes_table():
5+
conn = connections.get("default")
6+
await conn.execute_script(
7+
"""
8+
ALTER TABLE "filecodes" ADD "file_hash" VARCHAR(128);
9+
ALTER TABLE "filecodes" ADD "is_chunked" BOOL NOT NULL DEFAULT False;
10+
ALTER TABLE "filecodes" ADD "upload_id" VARCHAR(128);
11+
CREATE TABLE "uploadchunk" (
12+
id INTEGER not null primary key autoincrement,
13+
"upload_id" VARCHAR(36) NOT NULL,
14+
"chunk_index" INT NOT NULL,
15+
"chunk_hash" VARCHAR(128) NOT NULL,
16+
"total_chunks" INT NOT NULL,
17+
"file_size" BIGINT NOT NULL,
18+
"chunk_size" INT NOT NULL,
19+
"created_at" TIMESTAMPTZ NOT NULL,
20+
"file_name" VARCHAR(255) NOT NULL,
21+
"completed" BOOL NOT NULL
22+
);
23+
"""
24+
)
25+
26+
27+
async def migrate():
28+
await create_upload_chunk_and_update_file_codes_table()

apps/base/models.py

Lines changed: 32 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,119 +2,55 @@
22
# @Author : Lan
33
# @File : models.py
44
# @Software: PyCharm
5-
from datetime import datetime
65
from typing import Optional
76

8-
from tortoise import fields
97
from tortoise.models import Model
108
from tortoise.contrib.pydantic import pydantic_model_creator
119

10+
from tortoise import fields, models
11+
from datetime import datetime
1212
from core.utils import get_now
1313

1414

15-
class FileCodes(Model):
16-
id: Optional[int] = fields.IntField(pk=True)
17-
code: Optional[int] = fields.CharField(
18-
description="分享码", max_length=255, index=True, unique=True
19-
)
20-
prefix: Optional[str] = fields.CharField(
21-
max_length=255, description="前缀", default=""
22-
)
23-
suffix: Optional[str] = fields.CharField(
24-
max_length=255, description="后缀", default=""
25-
)
26-
uuid_file_name: Optional[str] = fields.CharField(
27-
max_length=255, description="uuid文件名", null=True
28-
)
29-
file_path: Optional[str] = fields.CharField(
30-
max_length=255, description="文件路径", null=True
31-
)
32-
size: Optional[int] = fields.IntField(description="文件大小", default=0)
33-
text: Optional[str] = fields.TextField(description="文本内容", null=True)
34-
expired_at: Optional[datetime] = fields.DatetimeField(
35-
null=True, description="过期时间"
36-
)
37-
expired_count: Optional[int] = fields.IntField(description="可用次数", default=0)
38-
used_count: Optional[int] = fields.IntField(description="已用次数", default=0)
39-
created_at: Optional[datetime] = fields.DatetimeField(
40-
auto_now_add=True, description="创建时间"
41-
)
42-
#
43-
# file_hash = fields.CharField(
44-
# max_length=128, # SHA-256需要64字符,这里预留扩展空间
45-
# description="文件哈希值",
46-
# unique=True,
47-
# null=True # 允许旧数据为空
48-
# )
49-
# hash_algorithm = fields.CharField(
50-
# max_length=20,
51-
# description="哈希算法类型",
52-
# null=True,
53-
# default="sha256"
54-
# )
55-
56-
# # 新增分片字段
57-
# chunk_size = fields.IntField(
58-
# description="分片大小(字节)",
59-
# default=0
60-
# )
61-
# total_chunks = fields.IntField(
62-
# description="总分片数",
63-
# default=0
64-
# )
65-
# uploaded_chunks = fields.IntField(
66-
# description="已上传分片数",
67-
# default=0
68-
# )
69-
# upload_status = fields.CharField(
70-
# max_length=20,
71-
# description="上传状态",
72-
# default="pending", # pending/in_progress/completed
73-
# choices=["pending", "in_progress", "completed"]
74-
# )
75-
# is_chunked = fields.BooleanField(
76-
# description="是否分片上传",
77-
# default=False
78-
# )
15+
class FileCodes(models.Model):
16+
id = fields.IntField(pk=True)
17+
code = fields.CharField(max_length=255, unique=True, index=True)
18+
prefix = fields.CharField(max_length=255, default="")
19+
suffix = fields.CharField(max_length=255, default="")
20+
uuid_file_name = fields.CharField(max_length=255, null=True)
21+
file_path = fields.CharField(max_length=255, null=True)
22+
size = fields.IntField(default=0)
23+
text = fields.TextField(null=True)
24+
expired_at = fields.DatetimeField(null=True)
25+
expired_count = fields.IntField(default=0)
26+
used_count = fields.IntField(default=0)
27+
created_at = fields.DatetimeField(auto_now_add=True)
28+
file_hash = fields.CharField(max_length=64, null=True)
29+
is_chunked = fields.BooleanField(default=False)
30+
upload_id = fields.CharField(max_length=36, null=True)
7931

8032
async def is_expired(self):
81-
# 按时间
8233
if self.expired_at is None:
8334
return False
8435
if self.expired_at and self.expired_count < 0:
8536
return self.expired_at < await get_now()
86-
# 按次数
87-
else:
88-
return self.expired_count <= 0
37+
return self.expired_count <= 0
8938

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

9342

94-
#
95-
# class FileChunks(Model):
96-
# id = fields.IntField(pk=True)
97-
# file_code = fields.ForeignKeyField(
98-
# "models.FileCodes",
99-
# related_name="chunks",
100-
# on_delete=fields.CASCADE
101-
# )
102-
# chunk_number = fields.IntField(description="分片序号")
103-
# chunk_hash = fields.CharField(
104-
# max_length=128,
105-
# description="分片哈希校验值"
106-
# )
107-
# chunk_path = fields.CharField(
108-
# max_length=255,
109-
# description="分片存储路径"
110-
# )
111-
# created_at = fields.DatetimeField(
112-
# auto_now_add=True,
113-
# description="上传时间"
114-
# )
115-
#
116-
# class Meta:
117-
# unique_together = [("file_code", "chunk_number")]
43+
class UploadChunk(models.Model):
44+
id = fields.IntField(pk=True)
45+
upload_id = fields.CharField(max_length=36, index=True)
46+
chunk_index = fields.IntField()
47+
chunk_hash = fields.CharField(max_length=64)
48+
total_chunks = fields.IntField()
49+
file_size = fields.BigIntField()
50+
chunk_size = fields.IntField()
51+
file_name = fields.CharField(max_length=255)
52+
created_at = fields.DatetimeField(auto_now_add=True)
53+
completed = fields.BooleanField(default=False)
11854

11955

12056
class KeyValue(Model):
@@ -129,3 +65,5 @@ class KeyValue(Model):
12965

13066

13167
file_codes_pydantic = pydantic_model_creator(FileCodes, name="FileCodes")
68+
upload_chunk_pydantic = pydantic_model_creator(UploadChunk, name="UploadChunk")
69+
key_value_pydantic = pydantic_model_creator(KeyValue, name="KeyValue")

apps/base/schemas.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,15 @@
33

44
class SelectFileModel(BaseModel):
55
code: str
6+
7+
8+
class InitChunkUploadModel(BaseModel):
9+
file_name: str
10+
chunk_size: int = 5 * 1024 * 1024
11+
file_size: int
12+
file_hash: str
13+
14+
15+
class CompleteUploadModel(BaseModel):
16+
expire_value: int
17+
expire_style: str

apps/base/utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import hashlib
23
import os
34
import uuid
45

@@ -29,8 +30,19 @@ async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]
2930
return path, suffix, prefix, file.filename, save_path
3031

3132

33+
async def get_chunk_file_path_name(file_name: str, upload_id: str) -> Tuple[str, str, str, str, str]:
34+
"""获取切片文件的路径和文件名"""
35+
today = datetime.datetime.now()
36+
storage_path = settings.storage_path.strip("/") # 移除开头和结尾的斜杠
37+
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{upload_id}"
38+
path = f"{storage_path}/{base_path}" if storage_path else base_path
39+
prefix, suffix = os.path.splitext(file_name)
40+
save_path = f"{path}/{prefix}{suffix}"
41+
return path, suffix, prefix, file_name, save_path
42+
43+
3244
async def get_expire_info(
33-
expire_value: int, expire_style: str
45+
expire_value: int, expire_style: str
3446
) -> Tuple[Optional[datetime.datetime], int, int, str]:
3547
"""获取过期信息"""
3648
expired_count, used_count = -1, 0
@@ -86,6 +98,18 @@ async def get_random_code(style="num") -> str:
8698
return code
8799

88100

101+
async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) -> str:
102+
sha = hashlib.sha256()
103+
await file.seek(0)
104+
while True:
105+
chunk = await file.read(chunk_size)
106+
if not chunk:
107+
break
108+
sha.update(chunk)
109+
await file.seek(0)
110+
return sha.hexdigest()
111+
112+
89113
ip_limit = {
90114
"error": IPRateLimit(count=settings.uploadCount, minutes=settings.errorMinute),
91115
"upload": IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute),

0 commit comments

Comments
 (0)