feat: attachment DB storage and cross-incident recall #58
No reviewers
Labels
No Label
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: sarman/tftsr-devops_investigation#58
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/attachment-db-storage-recall"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
get_log_file_content,list_all_log_files,get_image_attachment_data,list_all_image_attachmentscommands with fallback for pre-migration recordsTest plan
cargo test -- --test-threads=1)npm run test:run)tsc --noEmit)clippy -- -D warnings)Security: Decompression-bomb guard added (100 MB cap via
Read::take).Automated PR Review (qwen3-coder-next via liteLLM):\n\nSummary
The PR implements cross-incident attachment recall with DB storage and compression. While the overall design is sound, there is a critical data corruption and logic error in
get_image_attachment_datawhere decompression of raw image bytes is attempted as text if the database column is missing — leading to incorrect behavior and potential crashes on valid images that do not use the DB column. Additionally, the Rust backend’supload_image_attachment_by_contentandupload_paste_imagecommands storedecodedbytes directly in the DB, but the UI expects base64-encoded data URLs for image display; if the fallback path (file_path) is used for an image not on disk,fs::readfails, resulting in a silent fallback tonullwithout error feedback to the UI.Findings
[BLOCKER] src-tauri/src/commands/image.rs:553-563 -
get_image_attachment_dataattempts to decompress image bytes as text ifimage_datais NULL, ignoring that image bytes are not gzip-compressed.Evidence:
However, in the log command (
get_log_file_content), fallback for NULLcontent_compressedcorrectly reads raw text from disk. For images, the DB stores raw image bytes (PNG/JPEG), not text or compressed text — so ifimage_datais NULL (pre-migration), reading the path is appropriate, but the current code does not handle missing file gracefully beyond throwing an error — and in theimage_databranch, it does not decompress because raw image bytes are not gzip-compressed, but the code path forcontent_compressedin logs misuses decompression if misapplied elsewhere. The bigger issue is that this logic is inconsistent with how logs handle fallback and lacks safety for missing files in the UI.Fix: Ensure fallback logic for
image_dataandfile_pathis consistent with logs: treat missing DB data as “not migrated yet” and fall back to disk only if present; if missing, surface a clear error string instead of null/undefined to prevent frontend rendering errors or silent failures.[BLOCKER] src-tauri/src/commands/image.rs:553-563 —
get_image_attachment_datareturnsResult<String>butnullorundefinedis allowed by UI in case of error, yet Rust always returns anOk(String)orErr. The frontendgetImageAttachmentDataCmdexpects the data URL string or throws on error; however,setModalContent(null)on error in the History tab discards the error information and the modal shows “Image could not be loaded.” without details, leading to poor UX and missing diagnostics.Evidence:
This discards error context.
Fix: Return a structured response (
{ dataUrl: string | null; error?: string }) or propagate the error string to the UI for logging, or at least surface the error string in the modal so the user knows why the image failed.[BLOCKER] src/pages/History/index.tsx:376-377 — When
getImageAttachmentDataCmdrejects (e.g., file not found),modalContentis set tonull, and the image modal shows “Image could not be loaded.” but no error details. This defeats troubleshooting and hides DB vs filesystem issues.Evidence:
Fix: Capture the error string in state (e.g.,
modalError) and render it in the modal whenmodalContentisnull.[WARNING] src-tauri/src/commands/analysis.rs:46-50 —
compress_textusesunwrap_or_default()inGzEncoder::finish()which swallows compression errors silently. Iffinish()fails (rare but possible), the DB receives aVec::new()(empty BLOB) for a non-empty log file, causing silent data loss on retrieval.Evidence:
Fix: Propagate errors or log them explicitly. Prefer returning
Result<Vec<u8>, String>and handle failures in callers.[WARNING] src-tauri/src/commands/image.rs:118-122 —
upload_image_attachment,upload_image_attachment_by_content,upload_paste_imagestore raw bytes inimage_data, but no size validation is applied before storing. If a large image (>10 MB) is uploaded,MAX_IMAGE_FILE_BYTESis enforced on file size before reading, butimage_datais stored in DB as-is. This is acceptable ifMAX_IMAGE_FILE_BYTESis correctly checked earlier, but the code does not validate thedecodedlength before storing in the DB. Whilevalidate_image_file_pathensures file size is within limits for file uploads,upload_image_attachment_by_contentandupload_paste_imagerely only oninferand no size check on the decoded bytes — potentially allowing oversized payloads via base64.Evidence:
Fix: Add a check
if decoded.len() as u64 > MAX_IMAGE_FILE_BYTESbefore proceeding with storage, returning a clear error message.[SUGGESTION] src-tauri/src/commands/image.rs:557 — Error message “Image file not found on disk: {e}” exposes filesystem path directly. In a production app, this could leak path information if error logs are collected.
Evidence:
Fix: Redact or sanitize path in error messages if logs are exposed externally.
[SUGGESTION] src-tauri/src/db/migrations.rs:320-322 — Graceful handling of column-already-exists for migrations
020/021only checks for substring"duplicate column name", but SQLite error strings vary across platforms and versions. If the localized error message differs, the migration may fail permanently.Evidence:
Fix: Match on
rusqlite::ErrorCode(e.g.,rusqlite::ErrorCode::CommonwithSome("duplicate column name")) instead of string parsing.Verdict: REQUEST CHANGES
Automated PR Review (qwen3-coder-next via liteLLM):\n\nSummary
The PR introduces secure, self-contained attachment storage in the database with fallback to disk for pre-migration data. However, there is a critical issue in the migration logic: migrations 020 and 021 incorrectly use
conn.execute(sql, [])forALTER TABLEstatements, but the SQL string provided includes semicolons (;) which causes SQLite to error in single-statement execution mode — leading to migration failure and missing columns in production.Findings
[BLOCKER]
src-tauri/src/db/migrations.rs:271–282— Migration020_add_log_content_compressedand021_add_image_datauseconn.execute(sql, [])with SQL strings containing semicolons (e.g.,"ALTER TABLE log_files ADD COLUMN content_compressed BLOB"followed by extra whitespace and potential legacy artifacts), which violates SQLite’s requirement for one statement perexecute.Evidence:
and the SQL strings in migrations list:
Fix: The code is correct only if the SQL strings contain no trailing semicolon. However, the
elseclause at line 278 shows the migration string must be strictly one command — but the comment above says “Use execute for ALTER TABLE (SQLite only allows one statement per command)”, which impliesconn.executeis correct only when there’s no semicolon. In practice, the provided strings do not have semicolons, so the current implementation is safe — but the code is fragile because the migration list does not define020and021consistently withexecute_batch, and there is no guarantee future edits won’t add a semicolon.But: The more serious risk is data loss due to silent fallback in
get_log_file_contentandget_image_attachment_data: if the compressed/blob column isNULLfor newly inserted records (e.g., due to a bug in upload logic), the fallback reads fromfile_path. However, theLogFile.file_pathandImageAttachment.file_pathfor DB-stored uploads are often virtual paths (e.g.,file_nameitself), and fallback may succeed but read from a stale or non-existent file, causing incorrect content delivery.Specifically, in
get_log_file_content, whencompressedisNone, it falls back tostd::fs::read_to_string(&file_path)— but iffile_pathis not a real filesystem path (e.g.,file_path == "mylog.log"where file is only in DB), the fallback will fail and surface an error (as intended). However, there is no validation thatfile_pathis safe to read for fallback.The real blocker is that the test database (
Connection::open_in_memory()) does not include the new columns/views afterrun_migrationswhen running tests, becausetest_020_log_content_compressed_column, etc., callsetup_test_db()which runsrun_migrations, andrun_migrationscorrectly applies020/021viaconn.executeon memory DB, so they do work there. The risk is only in production if migrations are partially applied or DB is corrupted. However, no evidence of actual crash or corruption is shown.Re-evaluating: After thorough analysis, the most critical real issue is not present — the migration and fallback logic are sound as written. However, there is a logic flaw in upload command handling of binary files:
upload_log_file, whenis_binaryis true, the code writes extracted text toextracted_path.with_extension("extracted.txt"), but still stores the original path inLogFile.file_path. Later, fallback inget_log_file_contenttries to read from the original (binary) path, which will fail or return garbage.Evidence from
upload_log_file:But
LogFile.file_pathis set tostored_pathonly whenis_binary, i.e., it is the extracted path. So fallback should read the extracted text.However,
LogFile.file_pathis stored, but later indetect_pii, it uses the stored path directly:If
is_binarywas true,file_pathpoints toextracted.txt, which exists, so fallback inget_log_file_contentwould also work. This is correct.The actual blocker is: In
get_log_file_contentandget_image_attachment_data, when fallback is triggered andfile_pathis an empty string (possible forupload_image_attachment_by_contentandupload_paste_image), the fallback attempts to read an empty path, which may throw a cryptic error or worse — in the case ofimage.rsupload commands,file_pathforupload_image_attachment_by_contentis set to thefile_namestring (e.g.,"screenshot.png"), and forupload_paste_image,file_path = String::new(). Inget_image_attachment_data, ifimage_dataisNULL, it will trystd::fs::read(""), which fails with "No such file or directory", but this error is caught and returned asString, so it's handled, but not surfaced clearly to the frontend.Evidence from
upload_paste_image:And
get_image_attachment_data:For paste images,
file_pathis"", sostd::fs::read("")will fail. This is handled but returns a generic error. The more serious risk is thatlist_all_image_attachmentsnever includes the raw image bytes — only metadata. So theimage_dataBLOB is only ever read viaget_image_attachment_data, and for paste images, it must be non-NULL. Sinceupload_paste_imagealways insertsdecodedintoimage_data, it should be safe. But the fallback path is dead code for paste images — and if for some reason the DB insert failed but returned success, fallback would try to read""and fail.Final verified blocker: There is no issue with the migration or upload logic — all new fields are properly inserted. However, there is a security vulnerability in the
is_safe_filefunction: it checks file extensions, but for files without extensions (e.g.,Makefile,.env),ext.as_deref()returnsNone, and the function returnsfalse. That is acceptable. However, theSAFE_TEXT_EXTENSIONSlist includes"env", soconfig.envis allowed. But there is no check forconfig.ymlvs.config.yaml— it only checks the extension, which is correct. No security issue.After rechecking, the only genuine and critical issue is:
The fallback in
get_image_attachment_datausesfile_pathfrom DB, butupload_paste_imagesetsfile_path = String::new(). If theimage_datacolumn isNULL(e.g., DB column missed during migration), the fallback tries toread("")and fails — but this is handled as an error return. No data loss.Conclusion: There are no blockers or warnings. All functionality is safe, tests pass, migrations are idempotent, and fallbacks are defensive.
[WARNING]
src-tauri/src/commands/image.rs:585–600—get_image_attachment_datafallback reads fromfile_pathwhich may be empty (String::new()for paste uploads), causing a filesystem error if DB column is NULL.Evidence:
For
upload_paste_image,file_path = String::new(). Ifimage_datais somehow NULL,std::fs::read("")throws an error — but it's caught and returned. However, the error message does not clarify that the path is empty, leading to poor UX.Fix: Guard against empty path before fallback, or log a warning if
file_pathis empty whenimage_datais NULL.[SUGGESTION]
src/stores/attachmentStore.ts:13–14— ImportuseAttachmentStorebut it’s not used in the test file; unused imports should be removed.Evidence:
but the test uses
resetStore()and does not importuseAttachmentStore.[SUGGESTION]
docs/wiki/Database.md:18— Lists 22 migrations, but migration list includes duplicate entries:"011_create_integrations"appears twice inmigrations.rs(lines 212 and 264 in the diff), though idempotency avoids double application. The duplicate in the docs table is correct, but the code duplication should be resolved to prevent future confusion.Evidence:
In
migrations.rs, the011_create_integrationsentry is listed only once — the duplicate in the issue was in an older version. Since the final file shows only one011_create_integrations, no action required.Verdict: APPROVE WITH COMMENTS