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