Add stage-3 content triage: sort unknown files into binary/ and text/

Unknown files are no longer terminal. Stage 1 now routes :unknown onto a
dedicated queue (with the same blocking backpressure as the known queue),
and a third worker pool sorts each file into data/binary/ or data/text/
using a NUL-byte sniff of the first 8000 bytes.

- content.jl: is_binary content sniff (stage 3)
- worker.jl: handle_unknown_job; stage-1 routes unknown with backpressure;
  KNOWN_ENQUEUE_RETRY_SECONDS -> ROUTE_ENQUEUE_RETRY_SECONDS (serves both)
- config.jl: unknown_worker_count/queue_capacity, binary_dir, text_dir + env
- FileServer.jl: unknown queue, pool, stage-aware recovery, drain ordering
- tests for is_binary and handle_unknown_job; tmp_config isolates new dirs
- README: three-stage pipeline
This commit is contained in:
2026-07-02 16:54:17 -04:00
parent 2a46f5021a
commit e42e8ef8af
6 changed files with 227 additions and 83 deletions

View File

@@ -7,7 +7,8 @@ using JSON3
# directly rather than only through the HTTP surface.
using FileServer: Job, Config, ChannelQueue, enqueue!, dequeue!, length,
sanitize_filename, recover_dir!, normalize_metadata,
build_metadata, finalize_known!, run_exiftool
build_metadata, finalize_known!, run_exiftool,
is_binary, handle_unknown_job
# A minimal, valid 1×1 PNG. Lets the real-exiftool tests assert stable facts
# (FileType == "PNG", 1×1 dimensions) that don't drift across exiftool versions.
@@ -21,6 +22,8 @@ function tmp_config(root; kwargs...)
spool_dir = joinpath(root, "spool"),
known_dir = joinpath(root, "known"),
unknown_dir = joinpath(root, "unknown"),
binary_dir = joinpath(root, "binary"),
text_dir = joinpath(root, "text"),
done_dir = joinpath(root, "done"),
failed_dir = joinpath(root, "failed"),
kwargs...,
@@ -138,6 +141,52 @@ end
end
end
@testset "is_binary: NUL-byte sniff" begin
mktempdir() do root
# Plain text → text.
txt = joinpath(root, "notes.txt")
write(txt, "hello, world\nsecond line\n")
@test is_binary(txt) == false
# A NUL byte anywhere in the sniff window → binary.
bin = joinpath(root, "blob.dat")
write(bin, UInt8[0x01, 0x02, 0x00, 0x03])
@test is_binary(bin) == true
# Empty file has no NUL → treated as text.
empty = joinpath(root, "empty")
write(empty, UInt8[])
@test is_binary(empty) == false
# A NUL past the sniff window is not seen → still text.
far = joinpath(root, "far.txt")
write(far, vcat(fill(UInt8('a'), FileServer.CONTENT_SNIFF_BYTES), UInt8[0x00]))
@test is_binary(far) == false
end
end
@testset "handle_unknown_job: routes to binary/ and text/" begin
mktempdir() do root
cfg = tmp_config(root)
# A binary file (embedded NUL) lands in binary/.
bpath = joinpath(cfg.unknown_dir, "id-b-blob.dat")
write(bpath, UInt8[0x00, 0xFF, 0x10])
bjob = Job("id-b", "blob.dat", bpath, filesize(bpath), 0.0)
handle_unknown_job(bjob, cfg, 1)
@test isfile(joinpath(cfg.binary_dir, "id-b-blob.dat"))
@test !isfile(bpath)
# A text file lands in text/.
tpath = joinpath(cfg.unknown_dir, "id-t-notes.log")
write(tpath, "just some log text\n")
tjob = Job("id-t", "notes.log", tpath, filesize(tpath), 0.0)
handle_unknown_job(tjob, cfg, 1)
@test isfile(joinpath(cfg.text_dir, "id-t-notes.log"))
@test !isfile(tpath)
end
end
@testset "recover_dir!: re-enqueues work, skips sidecars" begin
mktempdir() do root
dir = joinpath(root, "known"); mkpath(dir)