Skip to content

Commit c0de1db

Browse files
authored
chore: Revert "feat: CLI: Native DuckDB file loading (#409)" (#470)
Signed-off-by: Ilya Boyandin <[email protected]>
1 parent adbfec4 commit c0de1db

3 files changed

Lines changed: 55 additions & 14 deletions

File tree

apps/sqlrooms-cli-ui/src/serverApi.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {PersistStorage, StorageValue} from 'zustand/middleware';
2+
import {RuntimeConfig} from './runtimeConfig';
23

34
type DuckDbLikeConnector = {
45
query: (sql: string) => PromiseLike<any>;
@@ -139,3 +140,51 @@ export function createDuckDbPersistStorage(
139140
},
140141
};
141142
}
143+
144+
function getApiBaseUrl(config: RuntimeConfig): string {
145+
return (config.apiBaseUrl || '').replace(/\/$/, '');
146+
}
147+
148+
const SAFE_PATH_RE = /^[A-Za-z0-9_\-./:\\]+$/;
149+
150+
/**
151+
* Validate and sanitize a server-returned file path to prevent SQL injection
152+
* when the path is later interpolated into DuckDB queries
153+
* (e.g. `read_ipc('${filePath}')`).
154+
*/
155+
function validateServerPath(raw: unknown): string {
156+
if (typeof raw !== 'string' || raw.length === 0) {
157+
throw new Error('Server returned an invalid upload path (not a string)');
158+
}
159+
160+
const normalized = raw.replace(/\\/g, '/');
161+
162+
if (!SAFE_PATH_RE.test(normalized)) {
163+
throw new Error(
164+
`Server returned an upload path with disallowed characters: ${normalized}`,
165+
);
166+
}
167+
168+
if (normalized.includes('..')) {
169+
throw new Error(
170+
`Server returned an upload path with directory traversal: ${normalized}`,
171+
);
172+
}
173+
174+
return normalized;
175+
}
176+
177+
export async function uploadFileToServer(
178+
file: File,
179+
config: RuntimeConfig,
180+
): Promise<string> {
181+
const uploadUrl = `${getApiBaseUrl(config)}/api/upload`;
182+
const form = new FormData();
183+
form.append('file', file, file.name);
184+
const res = await fetch(uploadUrl, {method: 'POST', body: form});
185+
if (!res.ok) {
186+
throw new Error(`Upload failed: ${res.statusText}`);
187+
}
188+
const data = (await res.json()) as {path: string};
189+
return validateServerPath(data.path);
190+
}

apps/sqlrooms-cli-ui/src/store.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import {createHttpDbBridge, DbConnection} from '@sqlrooms/db';
5151
import {getDefaultScaffoldTree} from './helpers';
5252
import {LAYOUT} from './layout';
5353
import {fetchRuntimeConfig} from './runtimeConfig';
54-
import {createDuckDbPersistStorage} from './serverApi';
54+
import {createDuckDbPersistStorage, uploadFileToServer} from './serverApi';
5555

5656
export const AppBuilderProjectConfig = z.object({
5757
appsBySheetId: z
@@ -111,20 +111,12 @@ const connector = createWebSocketDuckDbConnector({
111111
wsUrl: runtimeConfig.wsUrl || 'ws://localhost:4000',
112112
});
113113

114-
type FileWithNativePath = File & {path?: string};
115-
116-
function getCliFileReference(file: File): string {
117-
const nativePath = (file as FileWithNativePath).path;
118-
if (typeof nativePath === 'string' && nativePath.trim()) {
119-
return nativePath;
120-
}
121-
return file.name;
122-
}
123-
124114
const baseLoadFile = connector.loadFile.bind(connector);
125115
connector.loadFile = async (file, desiredTableName, options) => {
126116
if (file instanceof File) {
127-
return baseLoadFile(getCliFileReference(file), desiredTableName, options);
117+
const serverPath = await uploadFileToServer(file, runtimeConfig);
118+
const renamedFile = new File([file], serverPath, {type: file.type});
119+
return baseLoadFile(renamedFile, desiredTableName, options);
128120
}
129121
return baseLoadFile(file, desiredTableName, options);
130122
};

python/sqlrooms-cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ What happens:
1616

1717
- Starts the DuckDB websocket backend (from `sqlrooms-server`) on `ws://localhost:4000`.
1818
- Serves the AI example UI on `http://localhost:4173` and opens your browser (disable with `--no-open-browser`).
19-
- Drag-and-drop CSV/Parquet/DuckDB files to load them into DuckDB directly from your filesystem using the dropped file reference (no upload copy).
19+
- Drag-and-drop CSV/Parquet/DuckDB files to load them into DuckDB; files are uploaded to a local `sqlrooms_uploads` folder and referenced by path.
2020
- UI state is stored in the SQLRooms meta namespace (default `__sqlrooms`) of the selected DuckDB file.
2121

2222
## CLI flags
@@ -40,7 +40,7 @@ Tables created in the selected DuckDB file (or attached meta DB if `--meta-db` i
4040
- `__sqlrooms.ui_state` (one row: `key='default'`)
4141
- `__sqlrooms.sync_rooms` (only used when `--sync` is enabled)
4242

43-
Runtime config for the UI is exposed at `/api/config` / `/config.json`.
43+
Uploads go to `/api/upload`. Runtime config for the UI is exposed at `/api/config` / `/config.json`.
4444

4545
## Config file
4646

0 commit comments

Comments
 (0)