|
|
@@ -8,6 +8,7 @@ import logging
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
+
|
|
|
class StorageService:
|
|
|
def __init__(self, db: Session, gogs_client: GogsClient):
|
|
|
self.db = db
|
|
|
@@ -35,46 +36,88 @@ class StorageService:
|
|
|
self.db.refresh(version)
|
|
|
return version
|
|
|
|
|
|
- async def process_file_with_sha(self, version: DataVersion, relative_path: str, file_sha: str, owner: str, repo: str):
|
|
|
- """只处理变化的文件,未变化的文件不记录。"""
|
|
|
- # 查询同一项目 + 同一 stage + 同一文件路径的最新一条记录
|
|
|
+ def rollback_version(self, version: DataVersion):
|
|
|
+ """Remove a version and all its associated file records.
|
|
|
+
|
|
|
+ Used when a push didn't produce any data file changes,
|
|
|
+ meaning it was a code-only commit with no snapshot value.
|
|
|
+ """
|
|
|
+ self.db.query(DataFile).filter(DataFile.version_id == version.id).delete()
|
|
|
+ self.db.delete(version)
|
|
|
+ self.db.commit()
|
|
|
+ logger.info(f"Rolled back empty version {version.id}")
|
|
|
+
|
|
|
+ async def process_file_with_sha(
|
|
|
+ self,
|
|
|
+ version: DataVersion,
|
|
|
+ relative_path: str,
|
|
|
+ file_sha: str,
|
|
|
+ owner: str,
|
|
|
+ repo: str,
|
|
|
+ ) -> bool:
|
|
|
+ """Process a file and create a snapshot record.
|
|
|
+
|
|
|
+ **Snapshot semantics**: a record is ALWAYS created regardless of
|
|
|
+ whether the file changed. This ensures every version is a
|
|
|
+ self-contained snapshot of all declared output files.
|
|
|
+
|
|
|
+ Returns
|
|
|
+ -------
|
|
|
+ bool
|
|
|
+ ``True`` if the file content actually changed (new upload),
|
|
|
+ ``False`` if unchanged (record reuses previous OSS key).
|
|
|
+ """
|
|
|
+ # Find the latest record for this file in the same project + stage
|
|
|
last_file = (
|
|
|
self.db.query(DataFile)
|
|
|
.join(DataVersion)
|
|
|
.filter(
|
|
|
DataVersion.project_id == version.project_id,
|
|
|
DataVersion.stage == version.stage,
|
|
|
- DataFile.relative_path == relative_path
|
|
|
+ DataFile.relative_path == relative_path,
|
|
|
)
|
|
|
.order_by(DataVersion.created_at.desc())
|
|
|
.first()
|
|
|
)
|
|
|
|
|
|
if last_file and last_file.file_sha == file_sha:
|
|
|
- logger.info(f"File {relative_path} (SHA: {file_sha}) unchanged. Skipping.")
|
|
|
- return
|
|
|
+ # ── Unchanged: reuse previous OSS key, still record a snapshot entry ──
|
|
|
+ new_file = DataFile(
|
|
|
+ version_id=version.id,
|
|
|
+ relative_path=relative_path,
|
|
|
+ storage_path=last_file.storage_path,
|
|
|
+ file_size=last_file.file_size,
|
|
|
+ file_type=last_file.file_type,
|
|
|
+ file_sha=file_sha,
|
|
|
+ )
|
|
|
+ self.db.add(new_file)
|
|
|
+ self.db.commit()
|
|
|
+ logger.info(
|
|
|
+ f"File {relative_path} (SHA: {file_sha[:8]}…) "
|
|
|
+ f"unchanged — snapshot recorded, reusing OSS key"
|
|
|
+ )
|
|
|
+ return False
|
|
|
|
|
|
- # 文件是新的或有变化,下载并上传到 OSS
|
|
|
- logger.info(f"File {relative_path} (SHA: {file_sha}) changed. Downloading.")
|
|
|
+ # ── Changed or new: download → upload → record ──
|
|
|
+ logger.info(f"File {relative_path} (SHA: {file_sha[:8]}…) changed — downloading")
|
|
|
content = await self.gogs.get_file_content(owner, repo, version.commit_id, relative_path)
|
|
|
file_size = len(content)
|
|
|
|
|
|
project_name = version.project.project_name
|
|
|
+ oss_key = oss_client._build_key(
|
|
|
+ project_name, version.stage, version.commit_id, relative_path
|
|
|
+ )
|
|
|
|
|
|
- # 构建 OSS key
|
|
|
- oss_key = oss_client._build_key(project_name, version.stage, version.commit_id, relative_path)
|
|
|
-
|
|
|
- # 上传到 OSS
|
|
|
oss_client.upload(oss_key, content)
|
|
|
|
|
|
- # 创建记录(storage_path 存 OSS key)
|
|
|
new_file = DataFile(
|
|
|
version_id=version.id,
|
|
|
relative_path=relative_path,
|
|
|
storage_path=oss_key,
|
|
|
file_size=file_size,
|
|
|
file_type=os.path.splitext(relative_path)[1],
|
|
|
- file_sha=file_sha
|
|
|
+ file_sha=file_sha,
|
|
|
)
|
|
|
self.db.add(new_file)
|
|
|
self.db.commit()
|
|
|
+ return True
|