Skip to content

Commit c0cff7f

Browse files
committed
feat(filename): enhance item filename generation and extraction for hierarchical structure
1 parent fe3efe5 commit c0cff7f

1 file changed

Lines changed: 60 additions & 5 deletions

File tree

src/sync/item-packer.ts

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,75 @@ export function calculateChecksum(data: string): string {
8888

8989
/**
9090
* Generate filename for an item in storage.
91+
*
92+
* Item IDs come from category-loader.ts in format: {type}/{parent}/{file}.json
93+
* We transform these to a hierarchical structure for remote storage:
94+
* - Sessions: session/{projectHash}/{sessionId}.json → sessions/{projectHash}/{sessionId}.json.gz
95+
* - Messages: message/{sessionId}/{messageId}.json → messages/{sessionId}/{messageId}.json.gz
96+
* - Parts: part/{messageId}/{partId}.json → messages/parts/{messageId}/{partId}.json.gz
9197
*/
9298
export function getItemFilename(category: SyncCategory, itemId: string): string {
93-
// Sanitize item ID for filename (replace unsafe chars)
94-
const safeId = itemId.replace(/[/\\:*?"<>|]/g, '_');
99+
if (category === 'sessions') {
100+
// itemId: session/{projectHash}/{sessionId}.json
101+
// Output: sessions/{projectHash}/{sessionId}.json.gz
102+
const match = /^session\/([^/]+)\/(.+)\.json$/.exec(itemId);
103+
if (match?.[1] && match[2]) {
104+
return `sessions/${match[1]}/${match[2]}.json.gz`;
105+
}
106+
}
107+
108+
if (category === 'messages') {
109+
// Message: message/{sessionId}/{messageId}.json
110+
// Output: messages/{sessionId}/{messageId}.json.gz
111+
const msgMatch = /^message\/(ses_[^/]+)\/(.+)\.json$/.exec(itemId);
112+
if (msgMatch?.[1] && msgMatch[2]) {
113+
return `messages/${msgMatch[1]}/${msgMatch[2]}.json.gz`;
114+
}
115+
116+
// Part: part/{messageId}/{partId}.json
117+
// Output: messages/parts/{messageId}/{partId}.json.gz
118+
const partMatch = /^part\/(msg_[^/]+)\/(.+)\.json$/.exec(itemId);
119+
if (partMatch?.[1] && partMatch[2]) {
120+
return `messages/parts/${partMatch[1]}/${partMatch[2]}.json.gz`;
121+
}
122+
}
123+
124+
// Fallback: flat structure (replace path separators and unsafe chars)
125+
const safeId = itemId.replace(/[/\\:*?"<>|]/g, '_').replace(/\.json$/, '');
95126
return `${category}/${safeId}.json.gz`;
96127
}
97128

98129
/**
99130
* Extract item ID from filename.
131+
*
132+
* Reverses the hierarchical remote path back to the original item ID format
133+
* used by category-loader.ts.
100134
*/
101135
export function getItemIdFromFilename(filename: string): string | null {
102-
const regex = /^[^/]+\/(.+)\.json\.gz$/;
103-
const match = regex.exec(filename);
104-
return match ? (match[1] ?? null) : null;
136+
// Sessions: sessions/{projectHash}/{sessionId}.json.gz -> session/{projectHash}/{sessionId}.json
137+
const sessionMatch = /^sessions\/([^/]+)\/([^/]+)\.json\.gz$/.exec(filename);
138+
if (sessionMatch?.[1] && sessionMatch[2]) {
139+
return `session/${sessionMatch[1]}/${sessionMatch[2]}.json`;
140+
}
141+
142+
// Messages: messages/{sessionId}/{messageId}.json.gz -> message/{sessionId}/{messageId}.json
143+
const msgMatch = /^messages\/(ses_[^/]+)\/([^/]+)\.json\.gz$/.exec(filename);
144+
if (msgMatch?.[1] && msgMatch[2]) {
145+
return `message/${msgMatch[1]}/${msgMatch[2]}.json`;
146+
}
147+
148+
// Parts: messages/parts/{messageId}/{partId}.json.gz -> part/{messageId}/{partId}.json
149+
const partMatch = /^messages\/parts\/(msg_[^/]+)\/([^/]+)\.json\.gz$/.exec(filename);
150+
if (partMatch?.[1] && partMatch[2]) {
151+
return `part/${partMatch[1]}/${partMatch[2]}.json`;
152+
}
153+
154+
// Fallback: old flat format - restore slashes from underscores and add .json
155+
const fallbackMatch = /^[^/]+\/(.+)\.json\.gz$/.exec(filename);
156+
if (fallbackMatch?.[1]) {
157+
return fallbackMatch[1].replace(/_/g, '/') + '.json';
158+
}
159+
return null;
105160
}
106161

107162
/**

0 commit comments

Comments
 (0)