Skip to content

Commit b7e61fe

Browse files
committed
adding .sub to snap
1 parent df27880 commit b7e61fe

4 files changed

Lines changed: 73 additions & 2 deletions

File tree

src/core/fileProcessor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ class FileProcessor {
2626
static detectFormat(filePathOrBuffer: string | Buffer): FileFormat {
2727
if (typeof filePathOrBuffer === 'string') {
2828
const ext = path.extname(filePathOrBuffer).toLowerCase();
29+
const fileName = path.basename(filePathOrBuffer).toLowerCase();
30+
31+
// Handle double extensions like .sub.zip
32+
if (fileName.endsWith('.sub.zip') || ext === '.sub') {
33+
return 'snap';
34+
}
35+
2936
switch (ext) {
3037
case '.gridset':
3138
case '.gridsetx':

src/processors/snapProcessor.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import {
1717
import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
1818
import { SnapValidator } from '../validation/snapValidator';
1919
import { ValidationResult } from '../validation/validationTypes';
20-
import { ProcessorInput, getFs, getNodeRequire, getPath, isNodeRuntime } from '../utils/io';
20+
import { ProcessorInput, getFs, getNodeRequire, getPath, isNodeRuntime, getOs } from '../utils/io';
2121
import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
22+
import { openZipFromInput } from '../utils/zip';
2223

2324
/**
2425
* Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
@@ -123,8 +124,45 @@ class SnapProcessor extends BaseProcessor {
123124
await Promise.resolve();
124125
const tree = new AACTree();
125126
let dbResult: Awaited<ReturnType<typeof openSqliteDatabase>> | null = null;
127+
let cleanupTempZip: (() => void) | null = null;
128+
126129
try {
127-
dbResult = await openSqliteDatabase(filePathOrBuffer, { readonly: true });
130+
// Handle .sub.zip files (Snap pageset backups containing .sps files)
131+
let inputFile = filePathOrBuffer;
132+
133+
if (typeof filePathOrBuffer === 'string') {
134+
const fileName = getPath().basename(filePathOrBuffer).toLowerCase();
135+
if (fileName.endsWith('.sub.zip') || filePathOrBuffer.endsWith('.sub')) {
136+
const fs = getFs();
137+
const path = getPath();
138+
const os = getOs();
139+
140+
// Extract .sub.zip to find the embedded .sps file
141+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snap-sub-'));
142+
const { zip } = await openZipFromInput(filePathOrBuffer);
143+
144+
// Find the .sps file in the archive
145+
const files = zip.listFiles();
146+
const spsFile = files.find((f) => f.endsWith('.sps'));
147+
148+
if (!spsFile) {
149+
fs.rmSync(tempDir, { recursive: true, force: true });
150+
throw new Error('No .sps file found in .sub.zip archive');
151+
}
152+
153+
// Extract the .sps file
154+
const spsData = await zip.readFile(spsFile);
155+
const extractedSpsPath = path.join(tempDir, path.basename(spsFile));
156+
fs.writeFileSync(extractedSpsPath, Buffer.from(spsData));
157+
158+
inputFile = extractedSpsPath;
159+
cleanupTempZip = () => {
160+
fs.rmSync(tempDir, { recursive: true, force: true });
161+
};
162+
}
163+
}
164+
165+
dbResult = await openSqliteDatabase(inputFile, { readonly: true });
128166
const db = dbResult.db;
129167

130168
const getTableColumns = (tableName: string): Set<string> => {
@@ -789,6 +827,14 @@ class SnapProcessor extends BaseProcessor {
789827
} else if (dbResult?.db) {
790828
dbResult.db.close();
791829
}
830+
// Clean up temporary extracted .sps file from .sub.zip
831+
if (cleanupTempZip) {
832+
try {
833+
cleanupTempZip();
834+
} catch (e) {
835+
console.warn('[SnapProcessor] Failed to clean up temporary .sps file:', e);
836+
}
837+
}
792838
}
793839
}
794840

test/core/fileProcessor.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ describe('FileProcessor', () => {
125125
it('should detect snap format', async () => {
126126
expect(FileProcessor.detectFormat('test.sps')).toBe('snap');
127127
expect(FileProcessor.detectFormat('test.spb')).toBe('snap');
128+
expect(FileProcessor.detectFormat('test.sub')).toBe('snap');
129+
expect(FileProcessor.detectFormat('test.sub.zip')).toBe('snap');
130+
expect(FileProcessor.detectFormat('/path/to/My Pageset.sub.zip')).toBe('snap');
128131
});
129132

130133
it('should detect dot format', async () => {

test/snapProcessor.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { SnapProcessor } from '../src/processors/snapProcessor';
22
import { AACTree } from '../src/core/treeStructure';
33
import path from 'path';
4+
import fs from 'fs';
45

56
describe('SnapProcessor', () => {
67
const exampleFile: string = path.join(__dirname, 'assets/snap/example.spb');
78
const exampleSPSFile: string = path.join(__dirname, 'assets/snap/example.sps');
9+
const exampleSubZipFile: string = path.join(__dirname, 'assets/snap/example.sub.zip');
810

911
it('should extract all texts from a .spb file', async () => {
1012
const processor = new SnapProcessor();
@@ -58,6 +60,19 @@ describe('SnapProcessor', () => {
5860
}
5961
});
6062

63+
it('should handle .sub.zip files by extracting and processing the embedded .sps file', async () => {
64+
const processor = new SnapProcessor();
65+
// Skip test if example .sub.zip file doesn't exist
66+
if (!fs.existsSync(exampleSubZipFile)) {
67+
console.warn(`Skipping .sub.zip test - file not found: ${exampleSubZipFile}`);
68+
return;
69+
}
70+
const tree: AACTree = await processor.loadIntoTree(exampleSubZipFile);
71+
expect(tree).toBeTruthy();
72+
const pageIds: string[] = Object.keys(tree.pages);
73+
expect(pageIds.length).toBeGreaterThan(0);
74+
});
75+
6176
describe('Error Handling', () => {
6277
it('should throw error for non-existent file', async () => {
6378
const processor = new SnapProcessor();

0 commit comments

Comments
 (0)