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