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