server/router/fileserver/README.md
The fileserver package handles all binary file serving for Memos using native HTTP handlers. It was created to replace gRPC-based binary serving, which had limitations with HTTP range requests (required for Safari video/audio playback).
api/v1 package (single source of truth)fileserver/
├── fileserver.go # Main service and HTTP handlers
├── README.md # This file
└── fileserver_test.go # Tests (to be added)
GET /file/attachments/:uid/:filename[?thumbnail=true]
Parameters:
uid - Attachment unique identifierfilename - Original filenamethumbnail (optional) - Return thumbnail for imagesAuthentication: Required for non-public memos
Response:
200 OK - File content with proper Content-Type206 Partial Content - For range requests (video/audio)401 Unauthorized - Authentication required403 Forbidden - User not authorized404 Not Found - Attachment not foundHeaders:
Content-Type - MIME type of the fileCache-Control: public, max-age=3600Accept-Ranges: bytes - For video/audioContent-Range - For partial responses (206)GET /file/users/:identifier/avatar
Parameters:
identifier - User ID (e.g., 1) or username (e.g., steven)Authentication: Not required (avatars are public)
Response:
200 OK - Avatar image (PNG/JPEG)404 Not Found - User not found or no avatar setHeaders:
Content-Type - image/png or image/jpegCache-Control: public, max-age=3600The fileserver supports the following authentication methods:
JWT Access Token (Authorization: Bearer {token})
Personal Access Token (PAT) (Authorization: Bearer {pat})
Request → getCurrentUser()
├─→ Try Session Cookie
│ ├─→ Parse cookie value
│ ├─→ Get user from DB
│ ├─→ Validate session
│ └─→ Return user (if valid)
│
└─→ Try JWT Token
├─→ Parse Authorization header
├─→ Verify JWT signature
├─→ Get user from DB
├─→ Validate token in access tokens list
└─→ Return user (if valid)
Attachments:
Avatars:
serveAttachmentFile(c echo.Context) errorMain handler for attachment binary serving.
Flow:
serveUserAvatar(c echo.Context) errorMain handler for user avatar serving.
Flow:
getCurrentUser(ctx, c) (*store.User, error)Authenticates request using session cookie or JWT token.
authenticateBySession(ctx, cookie) (*store.User, error)Validates session cookie and returns authenticated user.
authenticateByJWT(ctx, token) (*store.User, error)Validates JWT access token and returns authenticated user.
checkAttachmentPermission(ctx, c, attachment) errorValidates user has permission to access attachment based on memo visibility.
getAttachmentBlob(attachment) ([]byte, error)Retrieves binary content from local storage, S3, or database.
getOrGenerateThumbnail(ctx, attachment) ([]byte, error)Returns cached thumbnail or generates new one (with semaphore limiting).
getUserByIdentifier(ctx, identifier) (*store.User, error)Finds user by ID (int) or username (string).
extractImageInfo(dataURI) (type, base64, error)Parses data URI to extract MIME type and base64 data.
github.com/labstack/echo/v5 - HTTP router and middlewaregithub.com/golang-jwt/jwt/v5 - JWT parsing and validationgithub.com/disintegration/imaging - Image thumbnail generationgolang.org/x/sync/semaphore - Concurrency control for thumbnailsserver/auth - Authentication utilitiesstore - Database operationsinternal/profile - Server configurationinternal/storage/s3 - S3 storage clientAuth-related constants are imported from server/auth:
auth.RefreshTokenCookieName - "memos_refresh"auth.PersonalAccessTokenPrefix - PAT identifier prefixPackage-specific constants:
ThumbnailCacheFolder - ".thumbnail_cache"thumbnailMaxSize - 600pxSupportedThumbnailMimeTypes - ["image/png", "image/jpeg"]All handlers return Echo HTTP errors with appropriate status codes:
// Bad request
echo.NewHTTPError(http.StatusBadRequest, "message")
// Unauthorized (no auth)
echo.NewHTTPError(http.StatusUnauthorized, "message")
// Forbidden (auth but no permission)
echo.NewHTTPError(http.StatusForbidden, "message")
// Not found
echo.NewHTTPError(http.StatusNotFound, "message")
// Internal error
echo.NewHTTPError(http.StatusInternalServerError, "message").SetInternal(err)
SVG and HTML files are served as application/octet-stream to prevent script execution:
if contentType == "image/svg+xml" ||
contentType == "text/html" ||
contentType == "application/xhtml+xml" {
contentType = "application/octet-stream"
}
Private content requires valid JWT access token or Personal Access Token.
Memo visibility rules enforced before serving attachments.
Thumbnails cached on disk to avoid regeneration:
{data_dir}/.thumbnail_cache/{attachment_id}{extension}Video/audio files use http.ServeContent() for efficient streaming:
All responses include cache headers:
Cache-Control: public, max-age=3600
S3 files served via presigned URLs (no server download).
See SAFARI_FIX.md for recommended test coverage.
# Test attachment
curl "http://localhost:8081/file/attachments/{uid}/file.jpg"
# Test avatar by username
curl "http://localhost:8081/file/users/steven/avatar"
# Test range request
curl -H "Range: bytes=0-999" "http://localhost:8081/file/attachments/{uid}/video.mp4"
See SAFARI_FIX.md section "Future Improvements" for planned enhancements.