返回 VideoClaw
storage.py
1 import json
2 import logging
3 import os
4 import shutil
5 import threading
6 import time
7 import uuid
8 from datetime import datetime
9 from typing import Any, Dict, Optional
10
11 from config import settings
12 from .events import publish_task_event
13
14 logger = logging.getLogger(__name__)
15 _task_store_lock = threading.RLock()
16
17 TASK_DATA_DIR = os.path.join(settings.CODE_DIR, "data", "tasks")
18 TASK_RESULT_DIR = os.path.join(settings.RESULT_DIR, "task")
19
20
21 def ensure_task_dirs() -> None:
22 os.makedirs(TASK_DATA_DIR, exist_ok=True)
23 os.makedirs(TASK_RESULT_DIR, exist_ok=True)
24
25
26 def new_task_id() -> str:
27 return f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}"
28
29
30 def task_metadata_path(task_id: str) -> str:
31 return os.path.join(TASK_DATA_DIR, f"{task_id}.json")
32
33
34 def task_output_dir(task_id: str) -> str:
35 return os.path.join(TASK_RESULT_DIR, task_id)
36
37
38 def now_iso() -> str:
39 return datetime.now().isoformat()
40
41
42 def save_task(metadata: Dict[str, Any]) -> None:
43 with _task_store_lock:
44 ensure_task_dirs()
45 path = task_metadata_path(metadata["task_id"])
46 tmp_path = f"{path}.{uuid.uuid4().hex}.tmp"
47 with open(tmp_path, "w", encoding="utf-8") as f:
48 json.dump(metadata, f, ensure_ascii=False, indent=2)
49 os.replace(tmp_path, path)
50
51
52 def load_task(task_id: str) -> Optional[Dict[str, Any]]:
53 with _task_store_lock:
54 path = task_metadata_path(task_id)
55 if not os.path.exists(path):
56 return None
57 with open(path, "r", encoding="utf-8") as f:
58 return json.load(f)
59
60
61 def delete_task(task_id: str) -> bool:
62 with _task_store_lock:
63 metadata = load_task(task_id)
64 if not metadata:
65 return False
66
67 metadata_path = task_metadata_path(task_id)
68 output_dir = metadata.get("output_dir") or task_output_dir(task_id)
69 if os.path.exists(metadata_path):
70 os.remove(metadata_path)
71 if output_dir and os.path.exists(output_dir):
72 shutil.rmtree(output_dir)
73 logger.info("Deleted pipeline task: task_id=%s output_dir=%s", task_id, output_dir)
74 return True
75
76
77 def list_tasks(limit: int = 100) -> list[Dict[str, Any]]:
78 with _task_store_lock:
79 ensure_task_dirs()
80 records = []
81 for filename in os.listdir(TASK_DATA_DIR):
82 if not filename.endswith(".json"):
83 continue
84 try:
85 with open(os.path.join(TASK_DATA_DIR, filename), "r", encoding="utf-8") as f:
86 records.append(json.load(f))
87 except Exception:
88 continue
89 records.sort(key=lambda item: item.get("created_at", ""), reverse=True)
90 return records[:limit]
91
92
93 def create_task(pipeline: str, input_params: Dict[str, Any]) -> Dict[str, Any]:
94 with _task_store_lock:
95 ensure_task_dirs()
96 task_id = new_task_id()
97 output_dir = task_output_dir(task_id)
98 os.makedirs(output_dir, exist_ok=True)
99 metadata = {
100 "task_id": task_id,
101 "pipeline": pipeline,
102 "status": "pending",
103 "progress": 0,
104 "message": "Task created",
105 "input": input_params,
106 "output": {},
107 "artifacts": [],
108 "error": None,
109 "created_at": now_iso(),
110 "updated_at": now_iso(),
111 "started_at": None,
112 "completed_at": None,
113 "duration_seconds": None,
114 "output_dir": output_dir,
115 }
116 save_task(metadata)
117 logger.info("Created pipeline task: task_id=%s pipeline=%s output_dir=%s", task_id, pipeline, output_dir)
118 return metadata
119
120
121 def update_task(task_id: str, **updates: Any) -> Dict[str, Any]:
122 with _task_store_lock:
123 metadata = load_task(task_id)
124 if not metadata:
125 raise FileNotFoundError(f"Task not found: {task_id}")
126 metadata.update(updates)
127 metadata["updated_at"] = now_iso()
128 save_task(metadata)
129 if "progress" in updates or "status" in updates:
130 logger.info(
131 "Task update: task_id=%s status=%s progress=%s message=%s",
132 task_id,
133 metadata.get("status"),
134 metadata.get("progress"),
135 metadata.get("message"),
136 )
137 publish_task_event(task_id, {
138 "type": "progress",
139 "status": metadata.get("status"),
140 "progress": metadata.get("progress", 0),
141 })
142 return metadata
143
144
145 def append_artifact(task_id: str, new_artifact: Dict[str, Any]) -> Dict[str, Any]:
146 with _task_store_lock:
147 metadata = load_task(task_id)
148 if not metadata:
149 raise FileNotFoundError(f"Task not found: {task_id}")
150
151 if not new_artifact.get("created_at"):
152 new_artifact = {**new_artifact, "created_at": now_iso()}
153
154 artifacts = list(metadata.get("artifacts") or [])
155 key = (new_artifact.get("kind"), new_artifact.get("name"), new_artifact.get("path"))
156 if not any((item.get("kind"), item.get("name"), item.get("path")) == key for item in artifacts):
157 artifacts.append(new_artifact)
158 logger.info(
159 "Task artifact: task_id=%s kind=%s name=%s path=%s",
160 task_id,
161 new_artifact.get("kind"),
162 new_artifact.get("name"),
163 new_artifact.get("path"),
164 )
165 metadata["artifacts"] = artifacts
166 metadata["updated_at"] = now_iso()
167 save_task(metadata)
168 publish_task_event(task_id, {
169 "type": "artifact",
170 "status": metadata.get("status"),
171 "progress": metadata.get("progress", 0),
172 "artifact": new_artifact,
173 })
174 return metadata
175
176
177 def mark_running(task_id: str) -> Dict[str, Any]:
178 return update_task(task_id, status="running", progress=1, message="Task running", started_at=now_iso())
179
180
181 def mark_completed(task_id: str, output: Dict[str, Any], artifacts: list[Dict[str, Any]]) -> Dict[str, Any]:
182 metadata = load_task(task_id) or {"task_id": task_id}
183 started_at = metadata.get("started_at")
184 duration = None
185 if started_at:
186 try:
187 duration = time.time() - datetime.fromisoformat(started_at).timestamp()
188 except Exception:
189 duration = None
190 existing_artifacts = list(metadata.get("artifacts") or [])
191 merged_artifacts = list(existing_artifacts)
192 seen = {
193 (item.get("kind"), item.get("name"), item.get("path"))
194 for item in merged_artifacts
195 }
196 for item in artifacts or []:
197 key = (item.get("kind"), item.get("name"), item.get("path"))
198 if key in seen:
199 continue
200 merged_artifacts.append({**item, "created_at": item.get("created_at") or now_iso()})
201 seen.add(key)
202
203 metadata = update_task(
204 task_id,
205 status="completed",
206 progress=100,
207 message="Task completed",
208 output=output,
209 artifacts=merged_artifacts,
210 error=None,
211 completed_at=now_iso(),
212 duration_seconds=duration,
213 )
214 publish_task_event(task_id, {
215 "type": "completed",
216 "status": "completed",
217 "progress": 100,
218 })
219 return metadata
220
221
222 def mark_failed(task_id: str, error: str) -> Dict[str, Any]:
223 metadata = update_task(
224 task_id,
225 status="failed",
226 message="Task failed",
227 error=error,
228 completed_at=now_iso(),
229 )
230 publish_task_event(task_id, {
231 "type": "failed",
232 "status": "failed",
233 "progress": metadata.get("progress", 0),
234 })
235 return metadata
236
236 lines PYTHON