agentleFS
RoastSlavRoastSlav/quickdrop/AGENTS.md

quickdrop

AGENTS.md433 starsChanged 10 months agoMITView on GitHub

Self-hosted file sharing: Spring Boot 4.1 + Thymeleaf + SQLite (Flyway, Hibernate community dialect) + Tailwind. Java 21.

We split this AGENTS.md into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Don't restate what the code does, don't add rationale blocks to routine patterns, and don't explain what a previous version got wrong.

    line 8
  2. Distinctive:

    Commit messages: subject line plus 2–3 short sentences.

    line 10
  3. Very specific:

    Run: java -jar target/quickdrop.jar → http://localhost:8080.

    line 14
  4. Very specific:

    Add new protected routes via the right interceptor: AdminPasswordSetupInterceptor (forces /admin/setup until admin pwd exists), …

    line 38

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

QuickDrop — Agent Guide

Self-hosted file sharing: Spring Boot 4.1 + Thymeleaf + SQLite (Flyway, Hibernate community dialect) + Tailwind. Java 21.

Code style

  • Comment only where the code is genuinely ambiguous — a non-obvious constraint, a surprising "why", a trap someone would otherwise re-introduce. One or two terse lines.
  • Don't restate what the code does, don't add rationale blocks to routine patterns, and don't explain what a previous version got wrong. That belongs in the commit message.
  • Commit messages: subject line plus 2–3 short sentences.

Build & run

  • Build: ./mvnw clean package (Windows: mvnw.cmd clean package). Output: target/quickdrop.jar.
  • Run: java -jar target/quickdrop.jar → http://localhost:8080.
  • Tailwind CSS: npm run tw:build rebuilds src/main/resources/static/css/tailwind.css from tailwind-input.css.
  • Test suite: ./mvnw test (JUnit/MockMvc, ~630 tests under src/test/java/) — runs on every CI build via the Jenkinsfile's Build and Test stage and gates Docker publish. Full testing guide (conventions, live e2e instances, browser testing, i18n): docs/TESTING.md. HTTP/security probes and browser exploratory testing are separate, on-demand passes (not part of the automatic CI gate) — see the same doc plus scripts/seed_e2e.py and the quickdrop-e2e* configs in .claude/launch.json for isolated live instances (ports 8081–8084). Write findings from such a pass to docs/test-reports/ — that path stays gitignored because reports may describe unpatched vulnerabilities. Browser-only logic that MockMvc cannot reach has a second, much smaller suite: npm test (node --test, no framework dependency) over src/test/js/, currently covering the archive-layout rules in upload/zip-builder.js. src/package.json exists only to mark that tree as ES modules for Node; it sits above the Maven resource root and is not packaged.
  • Logs at log/quickdrop.log, DB at db/quickdrop.db, uploads under files/, database backups under db/backups/, custom branding under db/branding/ (all auto-created by QuickdropApplication; see AppPaths).
  • Docker: roastslav/quickdrop:latest, exposes 8080; mount /app/db, /app/files, /app/log — backups and branding live under /app/db, so no separate volume is needed for either; omitting /app/db loses all three on container recreation.

Architecture (follow the layers)

  • Controllers (controller/): FileViewController (/file/** — upload form, list, detail, download, preview, history), AdminViewController (/admin/** — dashboard, file/paste management, settings, /admin/links merged short-links admin page), FileRestController (/api/file/** — chunked upload, upload status/abort, share APIs), PasteViewController (/file/paste/** — paste create/edit forms; paste viewing is still FileViewController), ShareViewController (/share/{token} — share landing page + key auth), ShortLinkRestController (/api/link/** — QR codes for any short link, POST /api/link creates a redirect link), ShortLinkViewController (/link/new — create page; /s/{code} and /s/{code}/go — resolver and interstitial-confirm for redirect links, upload-share links forward to /share/{token} unchanged), StorageMigrationController (/admin/storage-migration** — storage backend migration UI/API), BackupController (/admin/backups** — database backup schedule overview, on-demand backup, restore, delete, download), PasswordViewController, IndexViewController. Thymeleaf views in src/main/resources/templates/.
  • Services (service/) own all business logic — there is no single do-everything service anymore. Route file mutations through FileLifecycleService (save/delete/extend/hide/share-token issuance) so cache eviction, history logging, and notifications fire via its @CacheEvicts and private logHistory helper. Its generateShareToken/revokeShareToken methods are thin wrappers that delegate the actual work to ShortLinkService — kept in FileLifecycleService so no controller call site had to change when the short-link feature was carved out; add new upload-share-link logic to ShortLinkService, not here. Reads/authorization go through FileQueryService (@Cacheable list/detail queries). Streaming downloads and share-token redemption go through FileDownloadService, which does its own history logging/cache eviction for download events.
  • Upload pipeline is async: POST /api/file/upload-chunk → AsyncFileMergeService queues the chunk on a background thread pool (submitChunk(..., waitForCompletion=false)) and the controller returns immediately — null body for intermediate chunks, 202 Accepted {"status":"processing","uploadId":...} on the last chunk. The merge task writes chunks in order, running the stream through EncryptionService when password + encryption flag are set, then calls FileLifecycleService.saveFile on completion. Frontend must poll GET /api/file/upload-status/{uploadId} (AsyncFileMergeService.getUploadStatus, returns status/uuid/error/processedChunks/totalChunks) rather than expect a synchronous result; POST /api/file/upload-abort cancels an in-flight upload. Metadata fields: description, keepIndefinitely, password, hidden, fileSize, archive fields, isPaste. The archive fields (archiveUpload/archiveName/archiveManifest, still stored in the folder_upload/folder_name/folder_manifest columns via @Column — renaming those needs a migration for no gain) carry any browser-built archive, not only a picked directory. Folder-vs-loose-bundle is deliberately not stored: it is derived from the manifest by FileUtils.summarizeArchive, which must keep matching deriveRootFolder in upload/zip-builder.js — every file entry under one shared top-level directory means a folder, anything else a bundle. Change one and you must change the other, or a folder gets labelled a bundle on the very page showing its folder name. A bundle is named files-<yymd>.zip by the browser (bundleDateStamp) and gets a four-character code appended by FileLifecycleService#distinctBundleName on save — the server is the only place that can check the name is not already taken, so the browser never has to ask. Guardrails, all client-side constants except the last: MAX_ARCHIVE_FILES (1000) caps a selection, IN_MEMORY_ZIP_LIMIT (256 MB) is the point above which JSZip is abandoned for the streamed zip64 writer so a large archive is never materialised in the tab, INITIAL_TREE_LINES (500, archive-tree.js) collapses a long contents tree behind a "show more", and MAX_MANIFEST_LENGTH (1 MB, FileRestController) bounds the client-supplied manifest before it is parsed and stored. The max-file-size setting is enforced in three places, and only one of them is authoritative: the upload page checks the selection and then the built archive, MultipartConfig bounds a single chunk request (never the assembled upload, since every chunk is 4 MB), and AsyncFileMergeService counts the bytes it actually merges and fails the upload past the limit. Only the last one survives a caller that ignores the page — fileSize is the client's word, so the controller's early check on it is a courtesy to honest clients, not a control.
  • Download: /file/download/{uuid} streams with on-the-fly decryption and logs to activity_log (renamed from file_history_log) via ActivityLog/ActivityLogRepository. EventType now spans several categories (FILE: UPLOAD/DOWNLOAD/RENEWAL/DELETION, PASTE, SHARE, ADMIN, SYSTEM), not just upload/renewal/download. Share links: POST /api/file/share/{uuid} issues a token, redeemed at GET /share/{token} (landing page, ShareViewController) with actual bytes served from GET /api/file/download/{token} (FileRestController, decrements/expires, redirects back to /share/{token} if invalid). The share_token table/entity was renamed to short_link/ShortLink — a single-table-inheritance base (discriminator target_type) that today has one subtype, UploadShareLink (the renamed ShareTokenEntity, still served at /share/{token}). This is groundwork for a general URL-shortener feature (plain-URL redirect links) sharing the same code space, resolver, and admin page — see ShortLinkRepository for the field-rename mapping (file→upload, shareToken→code, tokenExpirationDate→expirationDate, numberOfAllowedDownloads→remainingUses) — repository method names were deliberately kept unchanged via explicit @Query bodies so existing call sites didn't need to change.
  • Storage/model: UUID filenames on disk at fileStoragePath (from settings); Upload is an abstract JOINED-inheritance base entity (table upload) holding shared metadata + optional BCrypt password hash + encrypted flag, extended by StoredFile (adds folder fields for browser-built ZIP archives — a picked folder or a multi-file selection; FileEntityView copies those fields plus a derived itemCount so the list pages, the admin table and the share landing page can all show what an archive holds — the contents tree itself is drawn by archive-tree.js, a self-mounting module any page can include) and Paste (adds immutable, editOnly).
  • QR codes: QrCodeService renders SVG by hand from zxing-core's raw module matrix (no AWT), and PNG by piping that SVG through SvgRasterizationService — reuses its existing hardening rather than a second image path. Served for any ShortLink via GET /api/link/{code}/qr.{svg,png} (ShortLinkRestController). fileView.html's share-link QR used to be generated client-side by a qrcode@1.4.4 script loaded from cdn.jsdelivr.net on every file-view page load — replaced with an <img> pointing at this endpoint, removing that third-party call. batik-codec must stay alongside batik-transcoder in pom.xml: SvgRasterizationService#rasterizeToPng writes through Batik's own ImageWriterRegistry/SPI framework, not plain javax.imageio, and that registry has no PNG writer without it — every test that mocks rasterizeToPng (nearly all of them) would still pass without this dependency, so removing it wouldn't be caught by the suite alone.
  • General link shortener: RedirectLink is the other ShortLink target type (plain URL redirects, vs. UploadShareLink). Created via ShortLinkService#createRedirectLink, which runs the destination through LinkGuard (normalize + safety-check) before persisting, and resolved via ShortLinkService#resolveAndRecordVisit, which re-runs LinkGuard.checkForRedirect (DNS-rebinding recheck) and only then increments useCount/decrements remainingUses. ShortLinkViewController's resolver route is /s/{code} — a literal segment, not a {prefix} path variable, even though the prefix is meant to become configurable later: a variable first segment structurally matches any two-segment path, including static assets like /js/app.js, and Spring resolves annotated controllers before the static-resource handler, so a {prefix} pattern there would silently 404 every two-segment static asset site-wide — this actually happened during development and was only caught by live-browser verification, not by any MockMvc test, since none of them fetch real static assets through the dispatcher. Making the prefix configurable later needs a fixed allowlist of literal routes or a regex excluding reserved words (ShortCodeService's reserved-word list), not a bare path variable. Viewing the interstitial (short-link-preview.html) does not consume the link's use budget — only GET /s/{code}/go (or the admin-session direct-redirect path, which skips the interstitial) does, mirroring how /share/{token} (view) is separate from /api/file/download/{token} (the actual consuming action). /admin/links (AdminViewController) is the merged admin page for both link types, switched via a kind=share|redirect query param — two separate, simpler ShortLinkRepository queries (findFiltered for UploadShareLink, findFilteredRedirectLinks for RedirectLink) feeding one Thymeleaf template (admin-links.html) and one JS controller (admin-links.js), deliberately not a single polymorphic query across both subtypes. /admin/share-links (the pre-merge URL) 302s to /admin/links. EventCategory.SHORTLINK (SHORTLINK_CREATE/SHORTLINK_VISIT/SHORTLINK_REVOKE/SHORTLINK_EXPIRE/SHORTLINK_BLOCKED) covers redirect-link analytics; visit logging (not the useCount/remainingUses counters, which always update) is gated by shortenerClickLoggingEnabled. ActivityLog gained a nullable shortLink relation (short_link_id) alongside its existing file relation for this.

Settings (single-row, id=1)

  • ApplicationSettingsService seeds defaults on startup (max 1 GB, 30-day life, files/logs paths, cron 0 0 2 * * *, encryption on, default home = upload, SMTP/Discord off, backupScheduleEnabled=false, backupCron=0 0 4 * * *, maxBackups=7, etc.) and re-schedules cleanup when cron changes. Never insert a second settings row.
  • Template-level feature flags (file list enabled, app password set, admin button, encryption on, previews, pastebin) are injected by GlobalControllerAdvice — read from there; don't duplicate.
  • Shortener settings: shortenerEnabled (default on, open to all users — matches uploadEnabled's posture), shortenerAdminOnly, shortenerCodeLength (default 5, RedirectLink only) and shareTokenLength (default 8, floored at 8, UploadShareLink only — guessing a share token yields the file, so it needs more entropy than a redirect code; both share one code space), shortenerCustomAliasEnabled/shortenerCustomAliasAdminOnly (aliases default admin-only even though the feature itself doesn't — a self-chosen slug is the impersonation risk a random code doesn't have), shortenerInterstitialMode (ALWAYS/NEVER/NON_ADMIN), shortenerDomainRuleMode/shortenerDomainRules (admin allow/blocklist, enforced by LinkGuard on both creation and every resolve — see its javadoc). shortenerEnabled/shortenerAdminOnly gate creation only (POST /api/link, GET /link/new); shortenerInterstitialMode and the domain rules keep applying to already-issued links even after the feature is disabled, since disabling stops minting new links but must not break ones already sent out — same posture as shareLinksEnabled. shortenerClickLoggingEnabled (default on) governs SHORTLINK_VISIT audit-log rows only, not the link's own use counters. trustedProxyEnabled (default off) governs whether FileUtils#getRequesterInfo honours X-Forwarded-For/X-Real-IP at all — every caller of that method (activity-log IP attribution, RateLimitInterceptor's bucket key) now goes through this single gate; trusting those headers unconditionally would let a direct client spoof its logged IP and defeat rate limiting by rotating the header per request, so it's opt-in for admins who've confirmed a real reverse proxy sits in front.
  • Threat-intelligence reputation checking: ReputationCheckService is the facade LinkGuard calls on both creation and every resolve — it collects every @Service implementing ReputationProvider via List<ReputationProvider> constructor injection (same auto-collection pattern as DelegatingStorageService for storage backends), so a fourth provider needs only its own class, nothing here. Three implementations, all shipped disabled: PhishingArmyProvider and UrlhausProvider extend AbstractHashFeedProvider (shared download/parse/two-tier-match machinery: a sorted long[] of Fnv1aHash values for the hot-path lookup, confirmed against the raw feed file on disk only on a tier-1 hit — see that class's javadoc for the memory-footprint reasoning); SafeBrowsingProvider has no feed and queries Google's hashes:search API live per lookup instead, sending only a 4-byte hash prefix (never the URL) and caching verdicts in a small bounded LRU. ReputationSyncService mirrors BackupService's dynamic-cron pattern to keep the two feed-based providers refreshed on reputationFeedCron. A provider can only be turned on through ApplicationSettingsService#acceptReputationProviderTerms — the general settings save can only turn one off (which also clears its …TermsAcceptedAt timestamp, so re-enabling always re-prompts the licence-acceptance modal on the settings page); this is enforced server-side in updateApplicationSettings, not just hidden in the UI. reputationCheckEnabled is the master switch; reputationFailClosed (default off — fail open) decides whether a provider that can't complete its check (feed never loaded, API unreachable) blocks or is skipped.

Security & interceptors

  • SecurityConfig: optional whole-app password at /password/login, BCrypt, CSRF cookie, permissive CORS. Session timeout applied per-session by SessionService#sessionCreated, reading the current setting live so a change takes effect for the next session created without a restart.
  • In-memory admin + file session tokens live in SessionService (not persisted — restarts invalidate).
  • Add new protected routes via the right interceptor: AdminPasswordSetupInterceptor (forces /admin/setup until admin pwd exists), AdminPasswordInterceptor (/admin/** + file history), FilePasswordInterceptor (redirects to /file/password/{uuid} when the file has a password and no valid session token).
  • RateLimitInterceptor (10/60s per IP+group, patterns in WebConfig): /share/** and /api/file/download/** share one share-download bucket — both consume the same share token, so separate buckets would double an enumerator's quota. Any new route that redeems a token belongs in that group too.

Migrations & persistence

  • Flyway migrations in src/main/resources/db/migration/V*__*.sql (currently up to V45 — check the directory for the latest). Baseline-on-migrate is enabled — append a new V{n+1}__...sql, never edit existing files. SQLite dialect is org.hibernate.community.dialect.SQLiteDialect.
  • Entities in entity/, Spring Data repos in repository/. Use @Transactional on service methods that mutate.

Pagination, search & cache

  • Public list (/file/list) and admin dashboard use page/size/query params backed by Page<Upload>-returning service methods on FileQueryService — reuse them and pass all three params through to templates.
  • Spring Cache keys are publicFiles, adminFiles, adminDeletedFiles, adminPastes, adminDeletedPastes, analytics, applicationSettings (see CacheConfig). Any upload, deletion, renewal, visibility/keep toggle, or download must evict the relevant ones — copy the @CacheEvict pattern from FileLifecycleService/FileDownloadService.

Frontend (templates, styling, dialogs)

  • Every page starts from fragments/layout.html, never a hand-written <head>. A page is:
    <head th:replace="~{fragments/layout :: head(~{::title}, ~{::head-extra})}">
        <title th:text="#{page.x.title.browser(${appName})}">X</title>
        <th:block th:fragment="head-extra"><!-- page-only assets, or pass ~{} above --></th:block>
    </head>
    <body>
    <th:block th:replace="~{fragments/layout :: chrome(false)}"></th:block>   <!-- skip link + nav + orbs -->
    <main id="main-content">…</main>
    <th:block th:replace="~{fragments/layout :: pageScripts}"></th:block>
    </body>
    head already supplies charset/viewport, theme preload, both stylesheets, favicon, tailwindTheme.js, toast.js, confirm.js, Alpine and htmx — don't re-include any of them per page. head-extra is emitted before Alpine core, because Alpine plugins must register before it initialises (upload.html's @alpinejs/collapse depends on this ordering). Pass chrome(true) on pages behind AdminPasswordInterceptor (the visitor is an authenticated admin, so the nav shows the full admin link set); chrome(false) elsewhere. /admin/password is the login gate itself, so it stays false. Pages with no nav at all (welcome, app-password, admin-backup-restoring) use the orbs fragment instead of chrome. Admin sub-pages get their "← Back" button from the backLink(href, label) fragment.
  • Styling goes in @layer components in tailwind-input.css, never an inline <style> block. Reach for the existing component classes (page-wrap*, card*, btn*, form-*, badge-*, data-table, stat-hero*, gate-icon*, modal*, markdown-content) before writing new utility soup. custom.css is only for third-party widget internals that can't go through tokens (EasyMDE/CodeMirror, highlight.js). Rebuild with npm run tw:build after any change, and commit the regenerated tailwind.css — it is a checked-in build artifact.
  • Every page's content container is .page-wrap, optionally plus one width modifier: page-wrap-medium (72rem), page-wrap-narrow (64rem), page-wrap-tight (32rem); bare .page-wrap is 80rem. It encodes the responsive padding (px-4 sm:px-6 lg:px-8, py-8 lg:py-12) once — don't hand-roll max-w-… mx-auto px-… on a page again.
  • Component-layer rules are author CSS and are not tree-shaken, so anything unused ships to every visitor. The layer is currently at zero dead classes; if you add a colour variant, use it or drop it. The only classes with no literal reference in templates/JS are .toast--* and .modal--*, which toast.js/confirm.js build as `toast--${kind}`/`modal--${tone}` — they carry comments saying so. Before deleting a class, grep th:classappend too: badge-warning and gate-icon--indigo were both referenced but never defined, rendering unstyled for a long time, which is the mirror image of the same drift.
  • Never call alert(), confirm(), or prompt(). js/confirm.js is loaded on every page and owns both:
    • Confirmations → window.confirmAction({title, body, emphasis, confirmLabel, cancelLabel, tone}) returns a Promise<boolean>; tone is danger/warning/primary. It focus-traps, closes on Escape/backdrop, restores focus, and focuses Cancel on open so a stray Enter can't confirm a destructive action. body and emphasis accept a string or an array of paragraphs and are always inserted as textContent, so interpolating a filename or URL is safe.
    • Prefer the declarative form where the action is a plain form or link — no page script at all: put data-confirm plus data-confirm-title/-body/-emphasis/-label/-cancel/-tone on the <form> (intercepts submit) or on a button/link (intercepts click). Both listeners are delegated from document, so markup swapped in by htmx or spa.js keeps working.
    • Plain "something went wrong" messages → window.notify(message, kind), which forwards to the existing window.toast. Shared button labels (confirm, cancel, delete, revoke) are translated once into window.QD_CONFIRM_I18N by the layout — read them from there instead of adding another per-page i18n blob.

Scheduling & notifications

  • @EnableScheduling + ScheduleService handles expiry deletion (cron from settings), missing-file row cleanup, and short-link expiry (cleanShortLinks, daily 03:30 — covers both UploadShareLink and RedirectLink; the former also gets its encrypted sidecar removed, the latter gets a SHORTLINK_EXPIRE audit-log row per link since it has no Upload to carry history on otherwise). Reschedule by calling its rescheduleCleanup(...) when cron changes.
  • NotificationService batches email + Discord webhook events per configured interval; call it from FileLifecycleService/FileDownloadService, not controllers.
  • BackupService mirrors ScheduleService's dynamic-cron pattern (own ThreadPoolTaskScheduler, reschedules on SettingsChangedEvent) but for database backups — opt-in, via VACUUM INTO (not a raw file copy, since WAL mode can leave data in the -wal sidecar). Prunes down to maxBackups after every backup or upload. uploadBackup lets an admin import an externally-supplied .db file (another instance, an off-site copy) into the same pool, under a generated filename so it's indistinguishable from a self-made backup. restoreBackup validates the candidate with PRAGMA quick_check before touching the live DB — every candidate, uploaded or self-made, there's no special-casing by origin — clears stale -wal/-shm sidecars, then copies the backup over db/quickdrop.db; BackupController logs BACKUP_CREATED/BACKUP_UPLOADED/BACKUP_RESTORED/BACKUP_FAILED (new EventType.ADMIN entries) and triggers a delayed System.exit(0) via the RestartTrigger seam (real impl: ProcessExitRestartTrigger) on a successful restore, relying on restart: unless-stopped to bring the process back.

i18n

  • Bundles: messages.properties (default/English), messages_bg.properties, messages_de.properties, messages_es.properties, messages_fr.properties, messages_it.properties, messages_ja.properties, messages_zh.properties. Add every new user-facing string to all eight (see docs/i18n.md). Resolver wired in I18nConfig.

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repositoryOne other file it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.