Commit Graph
97 Commits
Author SHA1 Message Date
kolaente d2ec3fefc5 fix(avatar): remove a replaced avatar's blob only after commit
Address review decision: SaveAvatar removed the old file's blob from
storage inside the transaction, so a later rollback left the user on an
avatar whose content was gone. The files row is still deleted in the
session; the blob and its previews go once the session commits.
2026-10-20 23:05:30 +02:00
kolaente dc2b7ae37d fix(files): drop the file row when storing its content fails
Address pr-swarm finding: avatar sync on login no longer rolls back on a
store error, so a blobless files row would be committed.
2026-10-20 23:05:30 +02:00
kolaente 52f9da005a fix(files): store local blobs 0600 in 0700 directories
Stored attachments, backgrounds and exports were readable by every OS user
on the host. Existing files keep their mode.

Refs GHSA-9rfw-7hv4-3vv3
2026-10-20 15:13:02 +02:00
kolaenteandTink cedd30ab9c refactor(files): export the stored preview size list
Prepares generating every preview size from one decode in models.
2026-10-19 20:33:06 +02:00
kolaenteandTink 09cd2ba732 fix(attachments): drop a preview stored after its file was deleted
Address review comment: a generation finishing after its file was deleted left an orphaned preview.
2026-10-19 20:33:06 +02:00
kolaenteandTink 685c31a1ae refactor(attachments): cut comment noise
Address pr-swarm comment sweep.
2026-10-19 20:33:06 +02:00
kolaenteandTink b4b146a75c refactor(files): parse preview sizes from the stored size list
Address pr-swarm finding: the valid sizes were listed in four places.
2026-10-19 20:33:06 +02:00
kolaenteandTink b101cbff74 fix(files): open s3 files without requiring a content length
Address pr-swarm finding: Open went through OpenWithSize, which failed on S3-compatible backends that answer without a Content-Length, breaking every original download there.
2026-10-19 20:33:06 +02:00
kolaenteandTink 72bc9dfdb6 fix(files): remove the previews of a file with it
Deleting a file removes every preview size. A preview that does not
exist is not an error, and one that cannot be removed is only logged.
2026-10-19 20:33:06 +02:00
kolaenteandTink 3a005fcf6c feat(files): store previews of a file in the file storage
Previews live under previews/<file id>_<size>, next to the files they
are derived from but apart from them, so nothing that reads files by
their database id ever sees one.
2026-10-19 20:33:06 +02:00
kolaenteandTink 39c95dae39 feat(files): open a stored file together with its size
Serving a stored file with a Content-Length otherwise needs a Stat next
to the Open, which is a second round trip on S3.
2026-10-19 20:33:06 +02:00
kolaenteandTink dc8f3c02b9 fix(files): write local files through a temporary file
A file was created under its final name and then filled, so a reader
could open it half written and a failed write left a truncated file
behind. That did not matter for uploads, which are only readable once
their database row is committed, but a stored preview is looked up by
its name alone and would be served truncated from then on.
2026-10-19 20:33:06 +02:00
kolaenteandTink 3986fa697b refactor(files): move the preview sizes to the files package (mechanical)
File storage needs to know every size a preview can be stored in so it
can remove them all without listing the storage. The models package
keeps its names as aliases.
2026-10-19 20:33:06 +02:00
kolaente e3b56634b4 fix(files)!: move ErrCodeFileIsTooLarge off the task sort param code
ErrCodeFileIsTooLarge shared 4013 with ErrCodeInvalidSortParam, which has
held the code since the sort order feature landed, so an oversized upload
rendered "The task sort param is invalid." in clients.

BREAKING CHANGE: uploading a file larger than the configured limit now
returns 4035 instead of 4013.
2026-09-06 00:12:30 +02:00
kolaente 37649d1dd2 test: expect ErrFileDoesNotExist from LoadFileByID in s3 integration test 2026-09-04 23:36:53 +02:00
kolaente fe52d4e23e fix: return 404 instead of 500 when a stored file is missing from disk
A file row whose blob is gone from storage (Sentry API-OSS-50) made
LoadFileByID return the raw storage path error, which the error handler
turned into a 500 and reported to Sentry. Return ErrFileDoesNotExist
instead and update the callers that matched on the path error.
2026-09-04 23:36:53 +02:00
kolaente cb813c46a0 fix(files): map ErrFileDoesNotExist to a 404 http error
It had no HTTPError() method, so it surfaced as a 500.
2026-09-04 23:36:53 +02:00
kolaente 7b4b6046ca fix(migration): bound vikunja file imports
Preflight over the declared uncompressed sizes (overflow-safe), a file
count cap and a per-user storage quota bound the import before
anything is read; the actual decompressed bytes are still counted
against the same budget while reading. Attachments and backgrounds
now stream through a lazy provider: one zip entry is decompressed
into a private temp file at a time. A failed import deletes the file
blobs it wrote, which were orphaned for every migrator before.
2026-08-31 18:37:03 +02:00
kolaenteandTink f2971cf3c6 test(files): cover Ensure and ValidateBasePath per backend
Local creates a missing base path and is idempotent; ValidateBasePath rejects a
missing path without creating it and rejects a regular file. Mem and s3 are
no-ops that never touch a client.
2026-08-29 20:59:41 +02:00
kolaenteandTink 2d01399158 refactor(files): move base path creation and validation onto FileStorage
filehandling.go decided "is this local storage?" in three places: backend
selection, ensureLocalBasePath, and ValidateFileStorage. The latter two
re-derived a decision their caller had already made, and ensureLocalBasePath's
whole body was guarded by a check its only caller had just performed.

Two code paths independently deriving the same fact is what produced the path
divergence and the over-broad directory creation earlier in this series. The
backend now owns both answers: Ensure creates what it needs, ValidateBasePath
checks it. No-ops for s3 and mem, beside the existing MkdirAll no-op.

config.FilesType is now read once, at backend selection. Pure refactor: error
strings and creation behaviour are unchanged.
2026-08-29 20:59:41 +02:00
kolaenteandTink bf5f915615 fix(files): bound S3 storage calls with the caller's context
The S3 backend passed context.Background() to every AWS call, so an unreachable
but well-formed endpoint blocked until the OS TCP timeout with no way to
interrupt it. Write, Stat and Remove now take the caller's context.
2026-08-29 20:27:06 +02:00
kolaenteandTink 21c0939157 test(files): restore the storage global after InitFileHandler
TestInitFileHandler_S3Configuration and TestInitFileHandler_LocalFilesystem
replaced the package-level mem storage and never put it back, so any test
ordered after them failed. Pre-existing; surfaced by running with -shuffle=on.
2026-08-29 20:27:06 +02:00
kolaenteandTink f42d1a6ca9 test: stop leaking config overrides between tests
Capturing a config value before init returns "", and Key.Set writes at viper's
override level, which outranks defaults. Restoring a captured value therefore
pinned database.path to "" for the rest of the process, and TestRestore resolved
it to the bare rootpath and failed with "is a directory".

Only visible when VIKUNJA_TESTS_USE_CONFIG=1, which CI sets for every database
except sqlite-in-memory, which is why every test-api job failed while local runs
passed. ResetForTests restores genuine fresh-process state instead.
2026-08-29 20:27:06 +02:00
kolaenteandTink 5d27f57cc9 fix(doctor): stop creating the files directory during diagnostics
Running `vikunja doctor` outside the install directory created a `files`
directory in the current working directory and then reported it as existing and
writable, which reads as a passing check against storage doctor made itself.

Use the read-only file handler init. A missing base directory is now reported
as a failed check instead of being silently created.
2026-08-29 20:27:06 +02:00
kolaenteandTink 3a15683264 refactor(files): split base path creation out of storage validation
ValidateFileStorage created the local base directory as a side effect of
checking it. Move that into ensureLocalBasePath so callers that only want to
inspect storage can do so without creating it.

InitFileHandler now calls both, so server and restore behavior is unchanged.
InitFileHandlerReadOnly sets up the storage backend and nothing else.
2026-08-29 20:27:06 +02:00
kolaente 044106508a fix: use caller's session in LoadFileMetaByID to prevent connection pool deadlock
Querying the global engine while the request already holds an open
transaction acquires a second pool connection. Under concurrent
attachment downloads (e.g. a kanban board full of image previews) all
pool connections end up held by transactions that each wait for an
extra connection that can never be freed, hanging every request until
restart.

The redundant meta load in the upload avatar provider is removed
entirely; the decoded image is all that path uses.
2026-08-16 17:57:52 +02:00
Robert SzulistandGitHub 7ae80c073b fix: GCS signing issue (#2920) 2026-07-02 17:38:37 +02:00
kolaenteandkolaente dc935f263c docs(api/v2): tag task attachment fields for the v2 schema 2026-06-10 10:22:39 +00:00
kolaenteandkolaente 9e3e884dac refactor(metrics): drop inline file count tracking
The file count is now read from the database on demand.
2026-05-30 13:48:01 +00:00
kolaenteandkolaente 94f42bd6b2 fix(files): derive file size from reader at creation boundary
Authoritative size now comes from the reader instead of the caller's
claim in CreateWithMimeAndSession. The migration import path accepts
attacker-controlled metadata (GHSA-qh78-rvg3-cv54), so trusting
realsize for the limit check allowed oversized uploads to be accepted
and stored.

measureReaderSize leaves the reader seeked to 0 so the measured value
matches the bytes storage backends will actually write.
2026-04-09 16:22:56 +00:00
kolaente 0e1f44e57e refactor: replace afero with FileStorage interface
Replace the github.com/spf13/afero dependency with a purpose-built
FileStorage interface (Open, Write, Stat, Remove, MkdirAll) with three
implementations: localStorage (with basePath), s3Storage (with key
prefix), and memStorage (for tests).

Each implementation owns its base path — callers pass only file IDs.
Delete s3fs.go, change File.File from afero.File to io.ReadCloser,
and fix duplication flows to buffer content for seeking.
2026-03-20 10:59:44 +01:00
b0ede53c05 fix: handle S3 backend in user export download
http.ServeContent requires io.ReadSeeker which S3 files don't support.
Add S3 branch using io.Copy with explicit headers, matching the pattern
already used in task attachment downloads.

Refs: #2347

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 10:59:44 +01:00
b065c62007 feat: replace afero-s3 with minimal S3 afero.Fs implementation
Replace the third-party afero-s3 library (and its temporary fork) with
a minimal in-tree afero.Fs implementation (~200 lines) that only supports
the three S3 operations Vikunja actually uses: Open (GetObject), Remove
(DeleteObject), and Stat (HeadObject).

The s3File implementation lazily opens a GetObject stream on first Read
and supports Seek by closing and re-opening the stream from the new offset,
matching the behavior of the fixed afero-s3 fork.

All other afero.Fs/File methods return ErrS3NotSupported since they are
never called in the S3 code path (writes already use direct PutObject).

This removes the fclairamb/afero-s3 dependency and the temporary replace
directive in go.mod entirely.

Closes #2347

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 10:59:44 +01:00
kolaente a043940e14 refactor: use config.ResolvePath for all rootpath-relative paths
Replaces ad-hoc path joining in files, config value loading, and
default config values with the centralized ResolvePath function.
2026-03-09 16:02:05 +01:00
kolaente 3dd2ba4aa4 feat: register Vikunja tables with db package at init 2026-03-04 15:37:54 +01:00
kolaenteandGitHub b6155d525c feat(cli): reorganize repair commands under unified 'vikunja repair' parent (#2300)
Consolidate four scattered repair/maintenance CLI commands into a unified `vikunja repair` parent command with subcommands.
2026-02-25 11:50:09 +00:00
kolaente 312648d7d6 fix: remove transaction control from File.Delete to prevent premature commit/rollback
File.Delete() had s.Commit() and s.Rollback() calls that could
prematurely commit or abort an outer transaction when using a shared
session. The caller is now responsible for transaction management.
2026-02-25 11:03:02 +01:00
kolaente 49bba7f830 fix: eliminate nested database sessions to prevent table locks
Refactor functions that created their own sessions when called from
within existing transactions, which caused "database table is locked"
errors in SQLite's shared-cache mode.

Changes:
- Add files.CreateWithSession() to reuse caller's session
- Refactor DeleteBackgroundFileIfExists() to accept session parameter
- Add variadic session parameter to notifications.Notify() and
  Notifiable.ShouldNotify() interface
- Update all Notify callers (~17 sites) to pass their session through
- Use files.CreateWithSession in SaveBackgroundFile and NewAttachment
- Fix test code to commit sessions before assertions
2026-02-25 11:03:02 +01:00
kolaente c9c250fb1c fix: add missing Commit() to write callers
After NewSession() auto-begins a transaction, callers that perform
writes must explicitly call Commit() for changes to persist. Without
this, writes are silently rolled back when Close() is called.

Affected callers:
- user deletion notification cron
- caldav token generation/deletion
- token cleanup cron
- mark-all-notifications-read endpoint
- saved filter view cron
- project background delete
- typesense reindex
- export cleanup cron
- task last-updated listener
- saved filter view listener
- SSO team cleanup cron
- migration status start/finish
- background set/remove handlers
- orphaned task position cleanup
- file creation
2026-02-25 11:03:02 +01:00
kolaente 55c122fb42 feat: add repair-file-mime-types CLI command
Add a CLI command that finds all files in the database with no MIME type
set, detects it from the stored file content, and updates the database.
This is useful for backfilling MIME types on files created before MIME
detection was added on upload.
2026-02-22 00:00:11 +01:00
kolaente 519f66a51f fix: detect and store mime type when creating file attachments
Use mimetype.DetectReader in files.Create to automatically detect and
store the MIME type from file content. This fixes task attachment
previews, S3 Content-Type headers, and UI display for newly uploaded
files.
2026-02-22 00:00:11 +01:00
kolaenteandGitHub b6974ffcfd feat: add UNSIGNED-PAYLOAD config option for S3-compatible stores (#2205)
Adds `files.s3.disablesigning` config option that sends
`UNSIGNED-PAYLOAD` instead of computing SHA256 hashes for S3 request
signing which fixes `XAmzContentSHA256Mismatch` errors with
S3-compatible providers like Ceph RadosGW and Clever Cloud Cellar

Resolves https://github.com/go-vikunja/vikunja/issues/2181
2026-02-08 15:03:19 +00:00
kolaente bcfde14b14 fix(backgrounds): stream unsplash download to temp file instead of memory
Use a temp file instead of io.ReadAll to avoid buffering the entire
Unsplash image in RAM, which could cause OOM with large images or
high maxsize configuration.
2026-02-08 15:31:25 +01:00
kolaente 0d395a9e5d refactor(files): remove redundant seek operations in writeToStorage
Move the seek-to-start into the local file branch only and simplify
contentLengthFromReadSeeker to seek to end then back to 0 instead of
saving/restoring the original position. This reduces the S3 upload
path from 5 seek operations to 2.
2026-02-08 15:31:25 +01:00
kolaente 41b511b322 fix(files): seek to start before writing for consistent behavior
Both local and S3 backends now seek to position 0 before writing,
ensuring consistent behavior regardless of the reader's current offset.
2026-02-08 15:31:25 +01:00
kolaente 82933a0836 test(files): update tests for io.ReadSeeker API
- Replace custom testfile structs with bytes.NewReader
- Remove readerOnly wrapper and non-seekable reader tests (no longer
  possible at the type level)
- Update S3 unit tests to remove temp file assertions
2026-02-08 15:31:25 +01:00
kolaente 728a3e4f7b fix(files): require io.ReadSeeker for S3 uploads, remove temp file fallback
The S3 upload path used temp files (vikunja-s3-upload-*) to buffer
non-seekable readers. In Docker containers with restrictive permissions,
these temp files could not be created, causing "permission denied"
errors for avatar and background image uploads.

By changing the file storage API (Create, CreateWithMime,
CreateWithMimeAndSession, Save) to require io.ReadSeeker instead of
io.Reader, the temp file fallback is no longer needed and is removed.
This enforces at the type level that all callers provide seekable
readers, preventing this class of bug from recurring.

Closes go-vikunja/vikunja#2185
2026-02-08 15:31:25 +01:00
acbf751ba0 feat(doctor): add user namespace detection and improved storage diagnostics (#2180)
This PR adds support for detecting and handling Linux user namespaces (commonly used in rootless Docker containers) and improves error diagnostics when file storage validation fails.

Docs PR: https://github.com/go-vikunja/website/pull/289

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-01 11:57:35 +01:00
XiangCanyandGitHub d238385199 fix(files): make sure base directory exists when using local file system (#2166)
Resolves  #2162
2026-01-27 13:11:44 +01:00
renovate[bot]andGitHub 843ef7218a fix(deps): update module github.com/fclairamb/afero-s3 to v0.4.0 (#2087) 2026-01-15 16:21:16 +01:00