systemprompt_sync/
files.rs1use serde::{Deserialize, Serialize};
6use std::fs;
7use std::path::{Path, PathBuf};
8use zip::ZipWriter;
9use zip::write::SimpleFileOptions;
10
11use crate::api_client::SyncApiClient;
12use crate::error::SyncResult;
13use crate::file_bundler::{
14 INCLUDE_DIRS, add_dir_to_zip, collect_files, compare_tarball_with_local, create_tarball,
15 extract_tarball, extract_tarball_selective, peek_manifest,
16};
17use crate::{SyncConfig, SyncDirection, SyncOperationResult};
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct FileBundle {
21 pub manifest: FileManifest,
22 #[serde(skip)]
23 pub data: Vec<u8>,
24}
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct FileManifest {
28 pub files: Vec<FileEntry>,
29 pub timestamp: chrono::DateTime<chrono::Utc>,
30 pub checksum: String,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct FileEntry {
35 pub path: String,
36 pub checksum: String,
37 pub size: u64,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
41pub enum FileDiffStatus {
42 Added,
43 Modified,
44 Deleted,
45 Unchanged,
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct SyncDiffEntry {
50 pub path: String,
51 pub status: FileDiffStatus,
52 pub size: u64,
53}
54
55#[derive(Debug)]
56pub struct SyncDiffResult {
57 pub entries: Vec<SyncDiffEntry>,
58 pub added: usize,
59 pub modified: usize,
60 pub deleted: usize,
61 pub unchanged: usize,
62}
63
64impl SyncDiffResult {
65 pub const fn has_changes(&self) -> bool {
66 self.added > 0 || self.modified > 0 || self.deleted > 0
67 }
68
69 pub fn changed_paths(&self) -> Vec<String> {
70 self.entries
71 .iter()
72 .filter(|e| e.status != FileDiffStatus::Unchanged)
73 .map(|e| e.path.clone())
74 .collect()
75 }
76}
77
78#[derive(Debug)]
79pub struct PullDownload {
80 pub data: Vec<u8>,
81 pub diff: SyncDiffResult,
82}
83
84#[derive(Debug)]
85pub struct FileSyncService {
86 config: SyncConfig,
87 api_client: SyncApiClient,
88}
89
90impl FileSyncService {
91 pub const fn new(config: SyncConfig, api_client: SyncApiClient) -> Self {
92 Self { config, api_client }
93 }
94
95 pub async fn sync(&self) -> SyncResult<SyncOperationResult> {
96 match self.config.direction {
97 SyncDirection::Push => self.push().await,
98 SyncDirection::Pull => self.pull().await,
99 }
100 }
101
102 pub async fn download_and_diff(&self) -> SyncResult<PullDownload> {
103 let services_path = PathBuf::from(&self.config.services_path);
104 let data = self
105 .api_client
106 .download_files(&self.config.tenant_id)
107 .await?;
108
109 let diff = compare_tarball_with_local(&data, &services_path)?;
110
111 Ok(PullDownload { data, diff })
112 }
113
114 pub fn backup_services(services_path: &Path) -> SyncResult<PathBuf> {
115 let project_root = services_path.parent().unwrap_or(services_path);
116 let backup_dir = project_root.join("backup");
117 fs::create_dir_all(&backup_dir)?;
118
119 let timestamp = chrono::Utc::now().format("%Y%m%d-%H%M%S");
120 let zip_path = backup_dir.join(format!("{timestamp}.zip"));
121
122 let file = fs::File::create(&zip_path)?;
123 let mut zip = ZipWriter::new(file);
124 let options =
125 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
126
127 for dir in INCLUDE_DIRS {
128 let dir_path = services_path.join(dir);
129 if dir_path.exists() {
130 add_dir_to_zip(&mut zip, &dir_path, services_path, options)?;
131 }
132 }
133
134 zip.finish()?;
135 Ok(zip_path)
136 }
137
138 pub fn apply(data: &[u8], services_path: &Path, paths: Option<&[String]>) -> SyncResult<usize> {
139 paths.map_or_else(
140 || extract_tarball(data, services_path),
141 |paths| extract_tarball_selective(data, services_path, paths),
142 )
143 }
144
145 async fn push(&self) -> SyncResult<SyncOperationResult> {
146 let services_path = PathBuf::from(&self.config.services_path);
147 let bundle = collect_files(&services_path)?;
148 let file_count = bundle.manifest.files.len();
149
150 if self.config.dry_run {
151 return Ok(SyncOperationResult::dry_run(
152 "files_push",
153 file_count,
154 serde_json::to_value(&bundle.manifest)?,
155 ));
156 }
157
158 let data = create_tarball(&services_path, &bundle.manifest)?;
159
160 let upload = self
161 .api_client
162 .upload_files(&self.config.tenant_id, data)
163 .await?;
164
165 Ok(SyncOperationResult::success(
166 "files_push",
167 upload.files_uploaded,
168 ))
169 }
170
171 async fn pull(&self) -> SyncResult<SyncOperationResult> {
172 let services_path = PathBuf::from(&self.config.services_path);
173 let data = self
174 .api_client
175 .download_files(&self.config.tenant_id)
176 .await?;
177
178 if self.config.dry_run {
179 let manifest = peek_manifest(&data)?;
180 return Ok(SyncOperationResult::dry_run(
181 "files_pull",
182 manifest.files.len(),
183 serde_json::to_value(&manifest)?,
184 ));
185 }
186
187 let count = extract_tarball(&data, &services_path)?;
188 Ok(SyncOperationResult::success("files_pull", count))
189 }
190}