|
1
|
"""Base interface for cloud source integrations.""" |
|
2
|
|
|
3
|
import logging |
|
4
|
from abc import ABC, abstractmethod |
|
5
|
from pathlib import Path |
|
6
|
from typing import List, Optional |
|
7
|
|
|
8
|
from pydantic import BaseModel, Field |
|
9
|
|
|
10
|
logger = logging.getLogger(__name__) |
|
11
|
|
|
12
|
|
|
13
|
class SourceFile(BaseModel): |
|
14
|
"""A file available in a cloud source.""" |
|
15
|
|
|
16
|
name: str = Field(description="File name") |
|
17
|
id: str = Field(description="Provider-specific file identifier") |
|
18
|
size_bytes: Optional[int] = Field(default=None, description="File size in bytes") |
|
19
|
mime_type: Optional[str] = Field(default=None, description="MIME type") |
|
20
|
modified_at: Optional[str] = Field(default=None, description="Last modified timestamp") |
|
21
|
path: Optional[str] = Field(default=None, description="Path within the source folder") |
|
22
|
|
|
23
|
|
|
24
|
class BaseSource(ABC): |
|
25
|
"""Abstract base class for cloud source integrations.""" |
|
26
|
|
|
27
|
@abstractmethod |
|
28
|
def authenticate(self) -> bool: |
|
29
|
"""Authenticate with the cloud provider. Returns True on success.""" |
|
30
|
... |
|
31
|
|
|
32
|
@abstractmethod |
|
33
|
def list_videos( |
|
34
|
self, |
|
35
|
folder_id: Optional[str] = None, |
|
36
|
folder_path: Optional[str] = None, |
|
37
|
patterns: Optional[List[str]] = None, |
|
38
|
) -> List[SourceFile]: |
|
39
|
"""List video files in a folder.""" |
|
40
|
... |
|
41
|
|
|
42
|
@abstractmethod |
|
43
|
def download( |
|
44
|
self, |
|
45
|
file: SourceFile, |
|
46
|
destination: Path, |
|
47
|
) -> Path: |
|
48
|
"""Download a file to a local path. Returns the local path.""" |
|
49
|
... |
|
50
|
|
|
51
|
def download_all( |
|
52
|
self, |
|
53
|
files: List[SourceFile], |
|
54
|
destination_dir: Path, |
|
55
|
) -> List[Path]: |
|
56
|
"""Download multiple files to a directory, preserving subfolder structure.""" |
|
57
|
destination_dir.mkdir(parents=True, exist_ok=True) |
|
58
|
paths = [] |
|
59
|
for f in files: |
|
60
|
# Use path (with subfolder) if available, otherwise just name |
|
61
|
relative = f.path if f.path else f.name |
|
62
|
dest = destination_dir / relative |
|
63
|
try: |
|
64
|
local_path = self.download(f, dest) |
|
65
|
paths.append(local_path) |
|
66
|
logger.info(f"Downloaded: {relative}") |
|
67
|
except Exception as e: |
|
68
|
logger.error(f"Failed to download {relative}: {e}") |
|
69
|
return paths |
|
70
|
|