@doubling/compound-sync 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/sync.js +76 -4
package/package.json
CHANGED
package/sync.js
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
import { initializeApp } from 'firebase/app';
|
|
18
18
|
import { getAuth, signInWithEmailAndPassword, signInWithCredential, GoogleAuthProvider } from 'firebase/auth';
|
|
19
19
|
import {
|
|
20
|
-
getFirestore, collection, doc, getDocs, getDoc, addDoc, updateDoc, deleteDoc,
|
|
20
|
+
getFirestore, collection, doc, getDocs, getDoc, setDoc, addDoc, updateDoc, deleteDoc,
|
|
21
21
|
query, where, onSnapshot
|
|
22
22
|
} from 'firebase/firestore';
|
|
23
|
+
import { getStorage, ref as storageRef, uploadString, deleteObject } from 'firebase/storage';
|
|
23
24
|
import { initializeAppCheck, CustomProvider } from 'firebase/app-check';
|
|
24
25
|
import chokidar from 'chokidar';
|
|
26
|
+
import crypto from 'crypto';
|
|
25
27
|
import fs from 'fs';
|
|
26
28
|
import path from 'path';
|
|
27
29
|
import readline from 'readline';
|
|
@@ -496,6 +498,43 @@ const LOCAL_PATH = expandHome(config.localPath || path.join(os.homedir(), 'Sync'
|
|
|
496
498
|
|
|
497
499
|
const filesRef = collection(db, `orgs/${ORG_ID}/files`);
|
|
498
500
|
|
|
501
|
+
// --- Cloud Storage for file blobs ---
|
|
502
|
+
//
|
|
503
|
+
// PR 2 of the Storage migration: dual-write file content to Cloud
|
|
504
|
+
// Storage in addition to the Firestore `content` field. Reads still
|
|
505
|
+
// come from Firestore until PR 5 cuts over. Storage rules require the
|
|
506
|
+
// file metadata document to exist, so the ordering on create is
|
|
507
|
+
// (1) write Firestore metadata, (2) upload to Storage, (3) update
|
|
508
|
+
// Firestore with the hash/path/size fields.
|
|
509
|
+
|
|
510
|
+
const storage = getStorage(app);
|
|
511
|
+
|
|
512
|
+
function computeContentHash(content) {
|
|
513
|
+
return crypto.createHash('sha256').update(content || '', 'utf-8').digest('hex');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function storagePathFor(fileId) {
|
|
517
|
+
return `orgs/${ORG_ID}/files/${fileId}/current.md`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function uploadBlob(fileId, content) {
|
|
521
|
+
const blobPath = storagePathFor(fileId);
|
|
522
|
+
await uploadString(storageRef(storage, blobPath), content || '', 'raw', {
|
|
523
|
+
contentType: 'text/markdown',
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function deleteBlob(fileId) {
|
|
528
|
+
const blobPath = storagePathFor(fileId);
|
|
529
|
+
try {
|
|
530
|
+
await deleteObject(storageRef(storage, blobPath));
|
|
531
|
+
} catch (err) {
|
|
532
|
+
if (err && err.code !== 'storage/object-not-found') {
|
|
533
|
+
console.error(` [storage] delete failed for ${blobPath}: ${err.message}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
499
538
|
// --- Load teams ---
|
|
500
539
|
|
|
501
540
|
const teams = new Map();
|
|
@@ -718,8 +757,15 @@ async function pushToFirestore(filePath, content) {
|
|
|
718
757
|
snapshot = await getDocs(query(filesRef, where('path', '==', docPath)));
|
|
719
758
|
}
|
|
720
759
|
|
|
760
|
+
const contentHash = computeContentHash(content);
|
|
761
|
+
const size = Buffer.byteLength(content || '', 'utf-8');
|
|
762
|
+
|
|
721
763
|
if (!snapshot || snapshot.empty) {
|
|
722
764
|
console.log(` [local → firestore] CREATE ${docPath} (${scope})`);
|
|
765
|
+
// Pre-generate a doc ref so we can compute the deterministic
|
|
766
|
+
// storagePath before the first Firestore write.
|
|
767
|
+
const newDocRef = doc(filesRef);
|
|
768
|
+
const fileId = newDocRef.id;
|
|
723
769
|
const fileData = {
|
|
724
770
|
path: docPath,
|
|
725
771
|
content,
|
|
@@ -729,14 +775,35 @@ async function pushToFirestore(filePath, content) {
|
|
|
729
775
|
teamId: scope === 'team' ? (teamId || null) : null,
|
|
730
776
|
ownerId: scope === 'private' ? USER_ID : null,
|
|
731
777
|
sharedWith: [],
|
|
778
|
+
storagePath: storagePathFor(fileId),
|
|
779
|
+
contentHash,
|
|
780
|
+
size,
|
|
732
781
|
};
|
|
733
|
-
await
|
|
782
|
+
await setDoc(newDocRef, fileData);
|
|
783
|
+
// Upload to Storage after Firestore so the Storage rule (which
|
|
784
|
+
// reads the file metadata to authorize) sees an existing doc.
|
|
785
|
+
try {
|
|
786
|
+
await uploadBlob(fileId, content);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
console.error(` [storage] upload failed for ${docPath}: ${err && err.message}`);
|
|
789
|
+
}
|
|
734
790
|
} else {
|
|
735
791
|
const docSnap = snapshot.docs[0];
|
|
736
792
|
const existing = docSnap.data();
|
|
737
|
-
if (existing.content === content) return;
|
|
793
|
+
if (existing.content === content && existing.contentHash === contentHash) return;
|
|
738
794
|
console.log(` [local → firestore] UPDATE ${docPath} (${scope})`);
|
|
739
|
-
await updateDoc(docSnap.ref, {
|
|
795
|
+
await updateDoc(docSnap.ref, {
|
|
796
|
+
content,
|
|
797
|
+
updatedAt: now,
|
|
798
|
+
storagePath: storagePathFor(docSnap.id),
|
|
799
|
+
contentHash,
|
|
800
|
+
size,
|
|
801
|
+
});
|
|
802
|
+
try {
|
|
803
|
+
await uploadBlob(docSnap.id, content);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
console.error(` [storage] upload failed for ${docPath}: ${err && err.message}`);
|
|
806
|
+
}
|
|
740
807
|
}
|
|
741
808
|
}
|
|
742
809
|
|
|
@@ -752,7 +819,12 @@ async function deleteFromFirestore(filePath) {
|
|
|
752
819
|
const data = snapshot.docs[0].data();
|
|
753
820
|
if ((data.scope || 'team') === scope) {
|
|
754
821
|
console.log(` [local → firestore] DELETE ${docPath} (${scope})`);
|
|
822
|
+
const fileId = snapshot.docs[0].id;
|
|
755
823
|
await deleteDoc(snapshot.docs[0].ref);
|
|
824
|
+
// Best-effort blob cleanup after the metadata is gone. Tolerates
|
|
825
|
+
// missing blobs (older files written before PR 2). Any leak is
|
|
826
|
+
// recoverable via a lifecycle rule or a cleanup script later.
|
|
827
|
+
await deleteBlob(fileId);
|
|
756
828
|
}
|
|
757
829
|
}
|
|
758
830
|
}
|