@@ -192,72 +192,91 @@ class CollabFolderSyncService {
192192 final folderPath = info.folderPath! ;
193193 final group = info.group;
194194 final syncMeta = await _readSyncMeta (folderPath);
195+ bool hasChanges = false ;
195196
196- // Find files not yet downloaded
197+ // --- Download new files ---
197198 final filesToDownload = group.files.where ((f) {
198- if (f.contentType == 'application/x-directory' ) return false ; // skip folder markers
199+ if (f.contentType == 'application/x-directory' ) return false ;
199200 return ! syncMeta.containsKey (f.id);
200201 }).toList ();
201202
202- if (filesToDownload.isEmpty) {
203- // No changes — back off poll interval (double, up to max)
204- final current = _currentPollInterval[groupId] ?? _minPollInterval;
205- if (current < _maxPollInterval) {
206- _currentPollInterval[groupId] = Duration (
207- seconds: (current.inSeconds * 2 ).clamp (
208- _minPollInterval.inSeconds,
209- _maxPollInterval.inSeconds,
210- ),
211- );
212- }
213- return ;
214- }
215-
216- // Changes found — reset to fast polling
217- _currentPollInterval[groupId] = _minPollInterval;
203+ if (filesToDownload.isNotEmpty) {
204+ hasChanges = true ;
205+ debugPrint ('[CollabFolderSync] Downloading ${filesToDownload .length } files for $groupId ' );
218206
219- debugPrint ('[CollabFolderSync] Downloading ${filesToDownload .length } files for $groupId ' );
207+ for (final file in filesToDownload) {
208+ try {
209+ final data = await CollaborationService .instance.downloadCollabFile (groupId, file);
210+ final localPath = _resolveLocalPath (folderPath, file);
211+ final localFile = File (localPath);
212+ await localFile.parent.create (recursive: true );
213+ await localFile.writeAsBytes (data);
220214
221- for (final file in filesToDownload) {
222- try {
223- final data = await CollaborationService .instance.downloadCollabFile (groupId, file);
224-
225- // Determine local file path (handle pathScope subfolders)
226- final localPath = _resolveLocalPath (folderPath, file);
227- final localFile = File (localPath);
228-
229- // Ensure parent directory exists
230- await localFile.parent.create (recursive: true );
231- await localFile.writeAsBytes (data);
232-
233- // Track in sync metadata
234- syncMeta[file.id] = _SyncedFileEntry (
235- fileName: file.fileName,
236- direction: 'download' ,
237- syncedAt: DateTime .now (),
238- );
239-
240- debugPrint ('[CollabFolderSync] Downloaded: ${file .fileName }' );
241- } catch (e) {
242- debugPrint ('[CollabFolderSync] Download failed (${file .fileName }): $e ' );
243- // Track 404 failures to avoid retrying missing files every poll
244- final errStr = e.toString ();
245- if (errStr.contains ('404' )) {
246215 syncMeta[file.id] = _SyncedFileEntry (
247216 fileName: file.fileName,
248- direction: 'download_failed ' ,
217+ direction: 'download ' ,
249218 syncedAt: DateTime .now (),
250219 );
220+ debugPrint ('[CollabFolderSync] Downloaded: ${file .fileName }' );
221+ } catch (e) {
222+ debugPrint ('[CollabFolderSync] Download failed (${file .fileName }): $e ' );
223+ final errStr = e.toString ();
224+ if (errStr.contains ('404' )) {
225+ syncMeta[file.id] = _SyncedFileEntry (
226+ fileName: file.fileName,
227+ direction: 'download_failed' ,
228+ syncedAt: DateTime .now (),
229+ );
230+ }
251231 }
252232 }
253233 }
254234
255- await _writeSyncMeta (folderPath, groupId, syncMeta);
235+ // --- Propagate remote deletions (tombstones) to local ---
236+ final removedIds = group.removedFileIds.toSet ();
237+ if (removedIds.isNotEmpty) {
238+ final entriesToRemove = < String > [];
239+ for (final entry in syncMeta.entries) {
240+ if (entry.value.direction == 'download_failed' ) continue ;
241+ if (removedIds.contains (entry.key)) {
242+ final localPath = '$folderPath ${Platform .pathSeparator }${entry .value .fileName }' ;
243+ final localFile = File (localPath);
244+ if (await localFile.exists ()) {
245+ try {
246+ await localFile.delete ();
247+ debugPrint ('[CollabFolderSync] Deleted local file (remote removal): ${entry .value .fileName }' );
248+ hasChanges = true ;
249+ } catch (e) {
250+ debugPrint ('[CollabFolderSync] Failed to delete local file: $localPath : $e ' );
251+ }
252+ }
253+ entriesToRemove.add (entry.key);
254+ }
255+ }
256+ for (final id in entriesToRemove) {
257+ syncMeta.remove (id);
258+ }
259+ }
260+
261+ // --- Adjust poll interval ---
262+ if (hasChanges) {
263+ _currentPollInterval[groupId] = _minPollInterval;
264+ await _writeSyncMeta (folderPath, groupId, syncMeta);
265+ } else {
266+ final current = _currentPollInterval[groupId] ?? _minPollInterval;
267+ if (current < _maxPollInterval) {
268+ _currentPollInterval[groupId] = Duration (
269+ seconds: (current.inSeconds * 2 ).clamp (
270+ _minPollInterval.inSeconds,
271+ _maxPollInterval.inSeconds,
272+ ),
273+ );
274+ }
275+ }
256276 } catch (e) {
257277 debugPrint ('[CollabFolderSync] Poll error ($groupId ): $e ' );
258278 } finally {
259279 _syncing[groupId] = false ;
260- // Reschedule next poll (only if sync is still active for this group)
261280 if (_watchers.containsKey (groupId)) {
262281 _schedulePoll (groupId);
263282 }
@@ -289,16 +308,25 @@ class CollabFolderSyncService {
289308 }
290309
291310 void _handleLocalFileChange (String groupId, FileSystemEvent event) {
292- // Only handle file creation/modification
293- if (event is ! FileSystemCreateEvent && event is ! FileSystemModifyEvent ) return ;
311+ // Handle file creation, modification, and deletion
312+ if (event is ! FileSystemCreateEvent &&
313+ event is ! FileSystemModifyEvent &&
314+ event is ! FileSystemDeleteEvent ) return ;
294315
295316 final path = event.path;
296317 final fileName = path.split (Platform .pathSeparator).last;
297318
298319 // Skip sync metadata and hidden files
299320 if (fileName == _syncMetaFile || fileName.startsWith ('.' )) return ;
300321
301- // Skip directories
322+ // Handle deletions immediately (file no longer exists on disk)
323+ if (event is FileSystemDeleteEvent ) {
324+ debugPrint ('[CollabFolderSync] File deletion detected: $fileName for group $groupId ' );
325+ _handleLocalFileDeletion (groupId, path);
326+ return ;
327+ }
328+
329+ // Skip directories (only for create/modify — deleted paths can't be checked)
302330 if (FileSystemEntity .isDirectorySync (path)) return ;
303331
304332 debugPrint ('[CollabFolderSync] File change detected: $fileName (${event .runtimeType }) for group $groupId ' );
@@ -311,6 +339,51 @@ class CollabFolderSyncService {
311339 });
312340 }
313341
342+ /// Handle deletion of a local file: find its collab ID from sync metadata,
343+ /// remove from the collaboration manifest, and clean up sync metadata.
344+ Future <void > _handleLocalFileDeletion (String groupId, String filePath) async {
345+ try {
346+ final info = await _getGroupInfo (groupId);
347+ if (info == null || info.folderPath == null ) return ;
348+
349+ final folderPath = info.folderPath! ;
350+ final fileName = filePath.split (Platform .pathSeparator).last;
351+ final syncMeta = await _readSyncMeta (folderPath);
352+
353+ // Find the file ID by matching fileName in sync metadata
354+ String ? matchedFileId;
355+ for (final entry in syncMeta.entries) {
356+ if (entry.value.fileName == fileName &&
357+ (entry.value.direction == 'upload' || entry.value.direction == 'download' )) {
358+ matchedFileId = entry.key;
359+ break ;
360+ }
361+ }
362+
363+ if (matchedFileId == null ) {
364+ debugPrint ('[CollabFolderSync] Deleted file not found in sync metadata: $fileName ' );
365+ return ;
366+ }
367+
368+ debugPrint ('[CollabFolderSync] Propagating deletion: $fileName (fileId=$matchedFileId )' );
369+ _emitStatus (groupId, CollabSyncState .syncing);
370+
371+ await CollaborationService .instance.removeFileFromGroup (
372+ groupId: groupId,
373+ fileId: matchedFileId,
374+ );
375+
376+ syncMeta.remove (matchedFileId);
377+ await _writeSyncMeta (folderPath, groupId, syncMeta);
378+
379+ debugPrint ('[CollabFolderSync] Deletion propagated: $fileName ' );
380+ _emitStatus (groupId, CollabSyncState .synced);
381+ } catch (e) {
382+ debugPrint ('[CollabFolderSync] Deletion propagation error ($filePath ): $e ' );
383+ _emitStatus (groupId, CollabSyncState .error);
384+ }
385+ }
386+
314387 Future <void > _uploadLocalFileIfNew (String groupId, String filePath) async {
315388 try {
316389 final info = await _getGroupInfo (groupId);
0 commit comments