From 9c009aecd1619706664f931ba52d225d88f2922c Mon Sep 17 00:00:00 2001 From: iltumio Date: Wed, 1 Apr 2026 12:25:10 +0200 Subject: [PATCH] chore: remove generated openapi.json from tracking, add test and docs - Remove resources/gen/openapi.json from git (served dynamically at /api/openapi.json) - Add resources/gen/ to .gitignore - Add OpenAPI spec validation test (paths, schemas, serialization) - Restore removed doc-comment on get_version - Fix cargo fmt violation in mod.rs import - Update CLAUDE.md: test count (~208), generate-openapi command, justfile reference --- .gitignore | 3 + CLAUDE.md | 6 +- resources/gen/openapi.json | 2020 ---------------------------------- src/interfaces/api/mod.rs | 60 +- src/interfaces/api/routes.rs | 1 + 5 files changed, 68 insertions(+), 2022 deletions(-) delete mode 100644 resources/gen/openapi.json diff --git a/.gitignore b/.gitignore index be2f265e..1467894e 100755 --- a/.gitignore +++ b/.gitignore @@ -78,5 +78,8 @@ storage/ *.swo nohup.out +# Generated files (OpenAPI spec, etc.) +resources/gen/ + # Helm chart dependencies charts/*/charts/* \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index e571eb10..111dd6e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,15 +8,19 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co cargo build # Dev build cargo build --release # Optimized release build cargo run # Run server (port 8086) -cargo test --workspace # Run all tests (~112) +cargo test --workspace # Run all tests (~208) cargo test # Run a single test by name cargo test --features test_utils # Run tests that use mockall mocks cargo clippy -- -D warnings # Lint (zero warnings policy) cargo fmt --all --check # Format check cargo fmt --all # Auto-format RUST_LOG=debug cargo run # Run with debug logging +cargo run --bin generate-openapi # Regenerate resources/gen/openapi.json ``` +A `justfile` is available for common tasks (`just --list` to see all). Key recipes: `just check` (fmt + clippy), `just test`, `just openapi`. + + Requires **Rust 1.93+** (edition 2024) and **PostgreSQL 13+** (with `pg_trgm` and `ltree` extensions). Database setup: `docker compose up -d postgres` — schema is applied automatically via sqlx migrations on app startup. Migration files live in `migrations/`. For local dev, set `DATABASE_URL` in `.env` (see `example.env`). diff --git a/resources/gen/openapi.json b/resources/gen/openapi.json deleted file mode 100644 index 91bd568b..00000000 --- a/resources/gen/openapi.json +++ /dev/null @@ -1,2020 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "OxiCloud API", - "description": "REST API for OxiCloud — self-hosted cloud storage, calendar & contacts", - "license": { - "name": "MIT" - }, - "version": "0.5.3" - }, - "paths": { - "/api/favorites": { - "get": { - "tags": [ - "favorites" - ], - "summary": "Handler for favorite-related API endpoints", - "operationId": "get_favorites", - "responses": { - "200": { - "description": "List of favorites", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FavoriteItemDto" - } - } - } - } - } - } - } - }, - "/api/favorites/batch": { - "post": { - "tags": [ - "favorites" - ], - "summary": "Add multiple items to favourites in a single transaction.\nPOST /api/favorites/batch", - "operationId": "batch_add_favorites", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchFavoritesRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Batch add result", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatchFavoritesResult" - } - } - } - }, - "400": { - "description": "Invalid request" - } - } - } - }, - "/api/favorites/{item_type}/{item_id}": { - "post": { - "tags": [ - "favorites" - ], - "summary": "Add an item to user's favorites", - "operationId": "add_favorite", - "parameters": [ - { - "name": "item_type", - "in": "path", - "description": "Item type (file or folder)", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "item_id", - "in": "path", - "description": "Item ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "201": { - "description": "Item added to favorites" - }, - "400": { - "description": "Invalid item type" - } - } - }, - "delete": { - "tags": [ - "favorites" - ], - "summary": "Remove an item from user's favorites", - "operationId": "remove_favorite", - "parameters": [ - { - "name": "item_type", - "in": "path", - "description": "Item type (file or folder)", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "item_id", - "in": "path", - "description": "Item ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Item removed from favorites" - }, - "404": { - "description": "Item not in favorites" - } - } - } - }, - "/api/recent": { - "get": { - "tags": [ - "recent" - ], - "summary": "Get user's recent items", - "operationId": "get_recent_items", - "responses": { - "200": { - "description": "List of recent items", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RecentItemDto" - } - } - } - } - } - } - } - }, - "/api/recent/clear": { - "delete": { - "tags": [ - "recent" - ], - "summary": "Clear all recent items", - "operationId": "clear_recent_items", - "responses": { - "200": { - "description": "Recent items cleared" - } - } - } - }, - "/api/recent/{item_type}/{item_id}": { - "post": { - "tags": [ - "recent" - ], - "summary": "Record access to an item", - "operationId": "record_item_access", - "parameters": [ - { - "name": "item_type", - "in": "path", - "description": "Item type (file or folder)", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "item_id", - "in": "path", - "description": "Item ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Access recorded" - }, - "400": { - "description": "Invalid item type" - } - } - }, - "delete": { - "tags": [ - "recent" - ], - "summary": "Remove an item from recents", - "operationId": "remove_from_recent", - "parameters": [ - { - "name": "item_type", - "in": "path", - "description": "Item type (file or folder)", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "item_id", - "in": "path", - "description": "Item ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Item removed from recents" - }, - "404": { - "description": "Item not in recents" - } - } - } - }, - "/api/s/{token}": { - "get": { - "tags": [ - "shares" - ], - "summary": "Access a shared item via its token", - "operationId": "access_shared_item", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Share token", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Shared item details" - }, - "401": { - "description": "Password required" - }, - "410": { - "description": "Share expired" - } - } - } - }, - "/api/s/{token}/verify": { - "post": { - "tags": [ - "shares" - ], - "summary": "Verify password for a password-protected shared item", - "operationId": "verify_shared_item_password", - "parameters": [ - { - "name": "token", - "in": "path", - "description": "Share token", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifyPasswordRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Password verified, item details returned" - }, - "401": { - "description": "Invalid password" - }, - "410": { - "description": "Share expired" - } - } - } - }, - "/api/shares": { - "get": { - "tags": [ - "shares" - ], - "summary": "Get all shared links created by the current user.\nSupports optional filtering by item_id + item_type query params.", - "operationId": "get_user_shares", - "responses": { - "200": { - "description": "List of shares", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ShareDto" - } - } - } - } - } - } - }, - "post": { - "tags": [ - "shares" - ], - "summary": "Create a new shared link", - "operationId": "create_shared_link", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateShareDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Share created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShareDto" - } - } - } - }, - "400": { - "description": "Bad request" - } - } - } - }, - "/api/shares/{id}": { - "get": { - "tags": [ - "shares" - ], - "summary": "Get information about a specific shared link by ID", - "operationId": "get_shared_link", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Share ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Share details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShareDto" - } - } - } - }, - "404": { - "description": "Share not found" - } - } - }, - "put": { - "tags": [ - "shares" - ], - "summary": "Update a shared link's properties", - "operationId": "update_shared_link", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Share ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateShareDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Share updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShareDto" - } - } - } - }, - "404": { - "description": "Share not found" - } - } - }, - "delete": { - "tags": [ - "shares" - ], - "summary": "Delete a shared link", - "operationId": "delete_shared_link", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Share ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "Share deleted" - }, - "404": { - "description": "Share not found" - } - } - } - }, - "/api/trash": { - "get": { - "tags": [ - "trash" - ], - "summary": "Gets all items in the trash for the current user", - "operationId": "get_trash_items", - "responses": { - "200": { - "description": "List of trashed items" - }, - "501": { - "description": "Trash feature not enabled" - } - } - } - }, - "/api/trash/empty": { - "delete": { - "tags": [ - "trash" - ], - "summary": "Empties the trash completely for the current user", - "operationId": "empty_trash", - "responses": { - "200": { - "description": "Trash emptied successfully" - }, - "501": { - "description": "Trash feature not enabled" - } - } - } - }, - "/api/trash/files/{id}": { - "delete": { - "tags": [ - "trash" - ], - "summary": "Moves a file to the trash", - "operationId": "move_file_to_trash", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "File ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "File moved to trash" - }, - "501": { - "description": "Trash feature not enabled" - } - } - } - }, - "/api/trash/folders/{id}": { - "delete": { - "tags": [ - "trash" - ], - "summary": "Moves a folder to the trash", - "operationId": "move_folder_to_trash", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Folder ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Folder moved to trash" - }, - "501": { - "description": "Trash feature not enabled" - } - } - } - }, - "/api/trash/{id}": { - "delete": { - "tags": [ - "trash" - ], - "summary": "Permanently deletes an item from the trash", - "operationId": "delete_permanently", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Trash item ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Item permanently deleted" - }, - "501": { - "description": "Trash feature not enabled" - } - } - } - }, - "/api/trash/{id}/restore": { - "post": { - "tags": [ - "trash" - ], - "summary": "Restores an item from the trash to its original location", - "operationId": "restore_from_trash", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "Trash item ID", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Item restored from trash" - }, - "501": { - "description": "Trash feature not enabled" - } - } - } - } - }, - "components": { - "schemas": { - "AuthResponseDto": { - "type": "object", - "required": [ - "user", - "access_token", - "refresh_token", - "token_type", - "expires_in" - ], - "properties": { - "access_token": { - "type": "string" - }, - "expires_in": { - "type": "integer", - "format": "int64" - }, - "refresh_token": { - "type": "string" - }, - "token_type": { - "type": "string" - }, - "user": { - "$ref": "#/components/schemas/UserDto" - } - } - }, - "BatchFavoriteItem": { - "type": "object", - "description": "Single item in a batch-add-favorites request.", - "required": [ - "item_id", - "item_type" - ], - "properties": { - "item_id": { - "type": "string" - }, - "item_type": { - "type": "string" - } - } - }, - "BatchFavoritesRequest": { - "type": "object", - "description": "Request body for POST /api/favorites/batch", - "required": [ - "items" - ], - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BatchFavoriteItem" - } - } - } - }, - "BatchFavoritesResult": { - "type": "object", - "description": "Result DTO for batch add-to-favorites.", - "required": [ - "stats", - "favorites" - ], - "properties": { - "favorites": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FavoriteItemDto" - }, - "description": "Full list of the user's favourites (enriched), so the client can\nreplace its local cache in a single round-trip." - }, - "stats": { - "$ref": "#/components/schemas/BatchFavoritesStats", - "description": "Statistics about the batch operation" - } - } - }, - "BatchFavoritesStats": { - "type": "object", - "required": [ - "requested", - "inserted", - "already_existed" - ], - "properties": { - "already_existed": { - "type": "integer", - "format": "int64", - "description": "How many were already favourites (skipped)", - "minimum": 0 - }, - "inserted": { - "type": "integer", - "format": "int64", - "description": "How many were actually inserted (new)", - "minimum": 0 - }, - "requested": { - "type": "integer", - "description": "How many items were requested", - "minimum": 0 - } - } - }, - "ChangePasswordDto": { - "type": "object", - "required": [ - "current_password", - "new_password" - ], - "properties": { - "current_password": { - "type": "string" - }, - "new_password": { - "type": "string" - } - } - }, - "CreateFolderDto": { - "type": "object", - "description": "DTO for folder creation requests", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "description": "Name of the folder to create" - }, - "parent_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID (None for root level)" - } - } - }, - "CreateShareDto": { - "type": "object", - "required": [ - "item_id", - "item_type" - ], - "properties": { - "expires_at": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "minimum": 0 - }, - "item_id": { - "type": "string" - }, - "item_name": { - "type": [ - "string", - "null" - ] - }, - "item_type": { - "type": "string" - }, - "password": { - "type": [ - "string", - "null" - ] - }, - "permissions": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/SharePermissionsDto" - } - ] - } - } - }, - "DeletePermanentlyRequest": { - "type": "object", - "description": "Request to permanently delete an item from trash", - "required": [ - "trash_id" - ], - "properties": { - "trash_id": { - "type": "string" - } - } - }, - "FavoriteItemDto": { - "type": "object", - "description": "DTO for favorites item, enriched with item metadata via SQL JOIN\nso the frontend does not need N+1 requests to resolve names/sizes.", - "required": [ - "id", - "user_id", - "item_id", - "item_type", - "created_at", - "icon_class", - "icon_special_class", - "category", - "size_formatted" - ], - "properties": { - "category": { - "type": "string", - "description": "Human-readable category (e.g. \"Image\", \"Folder\")" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "When the item was added to favorites" - }, - "icon_class": { - "type": "string", - "description": "FontAwesome icon CSS class (e.g. \"fas fa-file-image\", \"fas fa-folder\")" - }, - "icon_special_class": { - "type": "string", - "description": "Extra CSS class for icon styling (e.g. \"image-icon\", \"folder-icon\")" - }, - "id": { - "type": "string", - "description": "Unique identifier for the favorite entry" - }, - "item_id": { - "type": "string", - "description": "ID of the favorited item (file or folder)" - }, - "item_mime_type": { - "type": [ - "string", - "null" - ], - "description": "MIME type (files only)" - }, - "item_name": { - "type": [ - "string", - "null" - ], - "description": "Display name of the file or folder" - }, - "item_size": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Size in bytes (files only; folders → None)" - }, - "item_type": { - "type": "string", - "description": "Type of the item ('file' or 'folder')" - }, - "modified_at": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Last modification timestamp of the item" - }, - "parent_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID (folder_id for files, parent_id for folders)" - }, - "size_formatted": { - "type": "string", - "description": "Formatted file size (e.g. \"3.27 MB\"); \"--\" for folders" - }, - "user_id": { - "type": "string", - "description": "User ID who owns this favorite" - } - } - }, - "FileDto": { - "type": "object", - "description": "DTO for file responses", - "required": [ - "id", - "name", - "path", - "size", - "mime_type", - "created_at", - "modified_at", - "icon_class", - "icon_special_class", - "category", - "size_formatted" - ], - "properties": { - "category": { - "type": "string", - "description": "Human-readable file category (e.g. \"Image\", \"Document\")" - }, - "created_at": { - "type": "integer", - "format": "int64", - "description": "Creation timestamp", - "minimum": 0 - }, - "folder_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID" - }, - "icon_class": { - "type": "string", - "description": "FontAwesome icon CSS class (e.g. \"fas fa-file-image\")" - }, - "icon_special_class": { - "type": "string", - "description": "Extra CSS class for icon styling (e.g. \"image-icon\", \"\" when default)" - }, - "id": { - "type": "string", - "description": "File ID" - }, - "mime_type": { - "type": "string", - "description": "MIME type — `Arc` because MIME values repeat across files\nand DTOs are cloned on every request (clone is O(1) atomic increment)." - }, - "modified_at": { - "type": "integer", - "format": "int64", - "description": "Last modification timestamp", - "minimum": 0 - }, - "name": { - "type": "string", - "description": "File name" - }, - "owner_id": { - "type": [ - "string", - "null" - ], - "description": "Owner user ID (omitted from JSON when None)" - }, - "path": { - "type": "string", - "description": "Path to the file (relative)" - }, - "size": { - "type": "integer", - "format": "int64", - "description": "Size in bytes", - "minimum": 0 - }, - "size_formatted": { - "type": "string", - "description": "Human-readable formatted size (e.g. \"3.27 MB\")" - }, - "sort_date": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Sort date for Photos timeline — COALESCE(EXIF captured_at, created_at).\nOnly populated by the /api/photos endpoint.", - "minimum": 0 - } - } - }, - "FolderDto": { - "type": "object", - "description": "DTO for folder responses", - "required": [ - "id", - "name", - "path", - "created_at", - "modified_at", - "is_root", - "icon_class", - "icon_special_class", - "category" - ], - "properties": { - "category": { - "type": "string", - "description": "Human-readable category (always \"Folder\")" - }, - "created_at": { - "type": "integer", - "format": "int64", - "description": "Creation timestamp", - "minimum": 0 - }, - "icon_class": { - "type": "string", - "description": "FontAwesome icon CSS class (always \"fas fa-folder\")" - }, - "icon_special_class": { - "type": "string", - "description": "Extra CSS class for icon styling (always \"folder-icon\")" - }, - "id": { - "type": "string", - "description": "Folder ID" - }, - "is_root": { - "type": "boolean", - "description": "Whether this is a root folder" - }, - "modified_at": { - "type": "integer", - "format": "int64", - "description": "Last modification timestamp", - "minimum": 0 - }, - "name": { - "type": "string", - "description": "Folder name" - }, - "owner_id": { - "type": [ - "string", - "null" - ], - "description": "Owner user ID (scopes visibility per user)" - }, - "parent_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID" - }, - "path": { - "type": "string", - "description": "Path to the folder (relative)" - } - } - }, - "FolderListingDto": { - "type": "object", - "description": "Combined DTO that returns both sub-folders and files for a given folder\nin a single response, eliminating the double-fetch on every navigation.", - "required": [ - "folders", - "files" - ], - "properties": { - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDto" - }, - "description": "Files inside the requested folder" - }, - "folders": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FolderDto" - }, - "description": "Sub-folders inside the requested folder" - } - } - }, - "LoginDto": { - "type": "object", - "required": [ - "username", - "password" - ], - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "MoveFilePayload": { - "type": "object", - "description": "Payload for moving a file", - "properties": { - "folder_id": { - "type": [ - "string", - "null" - ], - "description": "Target folder ID (None means root)" - } - } - }, - "MoveFolderDto": { - "type": "object", - "description": "DTO for folder move requests", - "properties": { - "parent_id": { - "type": [ - "string", - "null" - ], - "description": "New parent folder ID (None for root level)" - } - } - }, - "MoveToTrashRequest": { - "type": "object", - "description": "Request to move an item to trash", - "required": [ - "item_id", - "item_type" - ], - "properties": { - "item_id": { - "type": "string" - }, - "item_type": { - "type": "string" - } - } - }, - "PaginationDto": { - "type": "object", - "description": "A DTO to represent pagination information", - "required": [ - "page", - "page_size", - "total_items", - "total_pages", - "has_next", - "has_prev" - ], - "properties": { - "has_next": { - "type": "boolean", - "description": "Indicates if there is a next page" - }, - "has_prev": { - "type": "boolean", - "description": "Indicates if there is a previous page" - }, - "page": { - "type": "integer", - "description": "Current page (starts at 0)", - "minimum": 0 - }, - "page_size": { - "type": "integer", - "description": "Page size", - "minimum": 0 - }, - "total_items": { - "type": "integer", - "description": "Total number of items", - "minimum": 0 - }, - "total_pages": { - "type": "integer", - "description": "Total number of pages", - "minimum": 0 - } - } - }, - "PaginationRequestDto": { - "type": "object", - "description": "A DTO to represent a pagination request", - "properties": { - "page": { - "type": "integer", - "description": "Requested page (starts at 0)", - "minimum": 0 - }, - "page_size": { - "type": "integer", - "description": "Requested page size", - "minimum": 0 - } - } - }, - "RecentItemDto": { - "type": "object", - "description": "DTO for recent items, enriched with item metadata via SQL JOIN\nso the frontend does not need N+1 requests to resolve names/sizes.", - "required": [ - "id", - "user_id", - "item_id", - "item_type", - "accessed_at", - "icon_class", - "icon_special_class", - "category", - "size_formatted" - ], - "properties": { - "accessed_at": { - "type": "string", - "format": "date-time", - "description": "When the item was accessed" - }, - "category": { - "type": "string", - "description": "Human-readable category (e.g. \"Image\", \"Folder\")" - }, - "icon_class": { - "type": "string", - "description": "FontAwesome icon CSS class (e.g. \"fas fa-file-image\", \"fas fa-folder\")" - }, - "icon_special_class": { - "type": "string", - "description": "Extra CSS class for icon styling (e.g. \"image-icon\", \"folder-icon\")" - }, - "id": { - "type": "string", - "description": "Unique identifier for the recent item" - }, - "item_id": { - "type": "string", - "description": "Item ID (file or folder)" - }, - "item_mime_type": { - "type": [ - "string", - "null" - ], - "description": "MIME type (files only)" - }, - "item_name": { - "type": [ - "string", - "null" - ], - "description": "Display name of the file or folder" - }, - "item_size": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Size in bytes (files only; folders → None)" - }, - "item_type": { - "type": "string", - "description": "Item type ('file' or 'folder')" - }, - "parent_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID (folder_id for files, parent_id for folders)" - }, - "size_formatted": { - "type": "string", - "description": "Formatted file size (e.g. \"3.27 MB\"); \"--\" for folders" - }, - "user_id": { - "type": "string", - "description": "Owner user ID" - } - } - }, - "RefreshTokenDto": { - "type": "object", - "required": [ - "refresh_token" - ], - "properties": { - "refresh_token": { - "type": "string" - } - } - }, - "RegisterDto": { - "type": "object", - "required": [ - "username", - "email", - "password" - ], - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "RenameFolderDto": { - "type": "object", - "description": "DTO for folder rename requests", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string", - "description": "New name for the folder" - } - } - }, - "RestoreFromTrashRequest": { - "type": "object", - "description": "Request to restore an item from trash", - "required": [ - "trash_id" - ], - "properties": { - "trash_id": { - "type": "string" - } - } - }, - "SearchCriteriaDto": { - "type": "object", - "description": "\n * Data Transfer Object for file search criteria.\n *\n * This structure represents all possible search parameters that can be used\n * to filter files and folders in the system. It supports various filter types\n * including name matching, file types, date ranges, and size constraints.", - "properties": { - "created_after": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Optional minimum creation date (seconds since epoch)", - "minimum": 0 - }, - "created_before": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Optional maximum creation date (seconds since epoch)", - "minimum": 0 - }, - "file_types": { - "type": [ - "array", - "null" - ], - "items": { - "type": "string" - }, - "description": "Optional list of file extensions to include (e.g., \"pdf\", \"jpg\")" - }, - "folder_id": { - "type": [ - "string", - "null" - ], - "description": "Optional folder ID to limit search scope" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results to return", - "minimum": 0 - }, - "max_size": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Optional maximum file size in bytes", - "minimum": 0 - }, - "min_size": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Optional minimum file size in bytes", - "minimum": 0 - }, - "modified_after": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Optional minimum modification date (seconds since epoch)", - "minimum": 0 - }, - "modified_before": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "description": "Optional maximum modification date (seconds since epoch)", - "minimum": 0 - }, - "name_contains": { - "type": [ - "string", - "null" - ], - "description": "Optional text to search in file/folder names" - }, - "offset": { - "type": "integer", - "description": "Offset for pagination", - "minimum": 0 - }, - "recursive": { - "type": "boolean", - "description": "Whether to search recursively within subfolders (default: true)" - }, - "sort_by": { - "type": "string", - "description": "Sort order for results: \"relevance\", \"name\", \"name_desc\", \"date\", \"date_desc\", \"size\", \"size_desc\"" - } - } - }, - "SearchFileResultDto": { - "type": "object", - "description": "A file search result enriched with server-computed metadata", - "required": [ - "id", - "name", - "path", - "size", - "mime_type", - "created_at", - "modified_at", - "relevance_score", - "size_formatted", - "icon_class", - "icon_special_class", - "category" - ], - "properties": { - "category": { - "type": "string", - "description": "Content category: \"document\", \"image\", \"video\", \"audio\", \"archive\", \"code\", \"other\"" - }, - "created_at": { - "type": "integer", - "format": "int64", - "description": "Creation timestamp", - "minimum": 0 - }, - "folder_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID" - }, - "icon_class": { - "type": "string", - "description": "CSS icon class for the file type (e.g., \"fas fa-file-pdf\")" - }, - "icon_special_class": { - "type": "string", - "description": "Extra CSS class for icon styling (e.g., \"pdf-icon\", \"code-icon js-icon\")" - }, - "id": { - "type": "string", - "description": "File ID" - }, - "mime_type": { - "type": "string", - "description": "MIME type" - }, - "modified_at": { - "type": "integer", - "format": "int64", - "description": "Last modification timestamp", - "minimum": 0 - }, - "name": { - "type": "string", - "description": "File name" - }, - "path": { - "type": "string", - "description": "Path to the file (relative)" - }, - "relevance_score": { - "type": "integer", - "format": "int32", - "description": "Relevance score (0-100) computed server-side", - "minimum": 0 - }, - "size": { - "type": "integer", - "format": "int64", - "description": "Size in bytes", - "minimum": 0 - }, - "size_formatted": { - "type": "string", - "description": "Human-readable file size (e.g., \"2.5 MB\")" - } - } - }, - "SearchFolderResultDto": { - "type": "object", - "description": "A folder search result enriched with server-computed metadata", - "required": [ - "id", - "name", - "path", - "created_at", - "modified_at", - "is_root", - "relevance_score" - ], - "properties": { - "created_at": { - "type": "integer", - "format": "int64", - "description": "Creation timestamp", - "minimum": 0 - }, - "id": { - "type": "string", - "description": "Folder ID" - }, - "is_root": { - "type": "boolean", - "description": "Whether it is a root folder" - }, - "modified_at": { - "type": "integer", - "format": "int64", - "description": "Last modification timestamp", - "minimum": 0 - }, - "name": { - "type": "string", - "description": "Folder name" - }, - "parent_id": { - "type": [ - "string", - "null" - ], - "description": "Parent folder ID" - }, - "path": { - "type": "string", - "description": "Path to the folder (relative)" - }, - "relevance_score": { - "type": "integer", - "format": "int32", - "description": "Relevance score (0-100) computed server-side", - "minimum": 0 - } - } - }, - "SearchResultsDto": { - "type": "object", - "description": "\n * Data Transfer Object for search results.\n *\n * This structure encapsulates the results of a search operation, including\n * both files and folders that match the search criteria, along with pagination\n * information and server-computed metadata.", - "required": [ - "files", - "folders", - "limit", - "offset", - "has_more", - "query_time_ms", - "sort_by" - ], - "properties": { - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchFileResultDto" - }, - "description": "Files matching the search criteria (enriched with metadata)" - }, - "folders": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchFolderResultDto" - }, - "description": "Folders matching the search criteria (enriched with metadata)" - }, - "has_more": { - "type": "boolean", - "description": "Whether there are more results available" - }, - "limit": { - "type": "integer", - "description": "Limit used in the search", - "minimum": 0 - }, - "offset": { - "type": "integer", - "description": "Offset used in the search", - "minimum": 0 - }, - "query_time_ms": { - "type": "integer", - "format": "int64", - "description": "Query execution time in milliseconds (server-side)", - "minimum": 0 - }, - "sort_by": { - "type": "string", - "description": "Sort order used" - }, - "total_count": { - "type": [ - "integer", - "null" - ], - "description": "Total count of matching items (for pagination)", - "minimum": 0 - } - } - }, - "SearchSuggestionItem": { - "type": "object", - "description": "Individual search suggestion item", - "required": [ - "name", - "item_type", - "id", - "path", - "icon_class", - "icon_special_class", - "relevance_score" - ], - "properties": { - "icon_class": { - "type": "string", - "description": "CSS icon class" - }, - "icon_special_class": { - "type": "string", - "description": "Extra CSS class for icon styling" - }, - "id": { - "type": "string", - "description": "Item ID for navigation" - }, - "item_type": { - "type": "string", - "description": "Type: \"file\" or \"folder\"" - }, - "name": { - "type": "string", - "description": "The suggested name" - }, - "path": { - "type": "string", - "description": "Path for context" - }, - "relevance_score": { - "type": "integer", - "format": "int32", - "description": "Relevance score", - "minimum": 0 - } - } - }, - "SearchSuggestionsDto": { - "type": "object", - "description": "DTO for search suggestion results (quick prefix search)", - "required": [ - "suggestions", - "query_time_ms" - ], - "properties": { - "query_time_ms": { - "type": "integer", - "format": "int64", - "description": "Query execution time in milliseconds", - "minimum": 0 - }, - "suggestions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchSuggestionItem" - }, - "description": "Suggested file/folder names matching the query prefix" - } - } - }, - "SetupAdminDto": { - "type": "object", - "description": "DTO for the one-time initial admin setup endpoint (`/api/setup`).\nAvailable only when the system is not yet initialized (no admin exists).", - "required": [ - "username", - "email", - "password" - ], - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - } - }, - "ShareDto": { - "type": "object", - "required": [ - "id", - "item_id", - "item_type", - "token", - "url", - "has_password", - "permissions", - "created_at", - "created_by", - "access_count" - ], - "properties": { - "access_count": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "created_at": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "created_by": { - "type": "string" - }, - "expires_at": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "minimum": 0 - }, - "has_password": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "item_id": { - "type": "string" - }, - "item_name": { - "type": [ - "string", - "null" - ] - }, - "item_type": { - "type": "string" - }, - "permissions": { - "$ref": "#/components/schemas/SharePermissionsDto" - }, - "token": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "SharePermissionsDto": { - "type": "object", - "required": [ - "read", - "write", - "reshare" - ], - "properties": { - "read": { - "type": "boolean" - }, - "reshare": { - "type": "boolean" - }, - "write": { - "type": "boolean" - } - } - }, - "TrashedItemDto": { - "type": "object", - "description": "DTO representing an item in the trash", - "required": [ - "id", - "original_id", - "item_type", - "name", - "original_path", - "trashed_at", - "days_until_deletion", - "category", - "icon_class", - "icon_special_class" - ], - "properties": { - "category": { - "type": "string", - "description": "Human-readable category (e.g., \"Image\", \"Folder\", \"Document\")" - }, - "days_until_deletion": { - "type": "integer", - "format": "int64" - }, - "icon_class": { - "type": "string", - "description": "FontAwesome icon class for the file type" - }, - "icon_special_class": { - "type": "string", - "description": "Special CSS class for icon styling (e.g., \"image-icon\", \"pdf-icon\")" - }, - "id": { - "type": "string" - }, - "item_type": { - "type": "string" - }, - "name": { - "type": "string" - }, - "original_id": { - "type": "string" - }, - "original_path": { - "type": "string" - }, - "trashed_at": { - "type": "string", - "format": "date-time" - } - } - }, - "UpdateShareDto": { - "type": "object", - "properties": { - "expires_at": { - "type": [ - "integer", - "null" - ], - "format": "int64", - "minimum": 0 - }, - "password": { - "type": [ - "string", - "null" - ] - }, - "permissions": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/SharePermissionsDto" - } - ] - } - } - }, - "UserDto": { - "type": "object", - "required": [ - "id", - "username", - "email", - "role", - "storage_quota_bytes", - "storage_used_bytes", - "created_at", - "updated_at", - "active", - "auth_provider" - ], - "properties": { - "active": { - "type": "boolean" - }, - "auth_provider": { - "type": "string" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "last_login_at": { - "type": [ - "string", - "null" - ], - "format": "date-time" - }, - "role": { - "type": "string" - }, - "storage_quota_bytes": { - "type": "integer", - "format": "int64" - }, - "storage_used_bytes": { - "type": "integer", - "format": "int64" - }, - "updated_at": { - "type": "string", - "format": "date-time" - }, - "username": { - "type": "string" - } - } - }, - "VerifyPasswordRequest": { - "type": "object", - "required": [ - "password" - ], - "properties": { - "password": { - "type": "string" - } - } - } - } - }, - "tags": [ - { - "name": "folders", - "description": "Folder management endpoints" - }, - { - "name": "files", - "description": "File management endpoints" - }, - { - "name": "trash", - "description": "Trash / recycle bin endpoints" - }, - { - "name": "search", - "description": "Search endpoints" - }, - { - "name": "shares", - "description": "Shared links endpoints" - }, - { - "name": "favorites", - "description": "Favorites management endpoints" - }, - { - "name": "recent", - "description": "Recent items endpoints" - } - ] -} \ No newline at end of file diff --git a/src/interfaces/api/mod.rs b/src/interfaces/api/mod.rs index e6a674d2..f53dde99 100755 --- a/src/interfaces/api/mod.rs +++ b/src/interfaces/api/mod.rs @@ -11,7 +11,9 @@ use crate::application::dtos::favorites_dto::{ BatchFavoritesResult, BatchFavoritesStats, FavoriteItemDto, }; use crate::application::dtos::file_dto::FileDto; -use crate::application::dtos::folder_dto::{CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto}; +use crate::application::dtos::folder_dto::{ + CreateFolderDto, FolderDto, MoveFolderDto, RenameFolderDto, +}; use crate::application::dtos::folder_listing_dto::FolderListingDto; use crate::application::dtos::pagination::{PaginationDto, PaginationRequestDto}; use crate::application::dtos::recent_dto::RecentItemDto; @@ -119,3 +121,59 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload; ) )] pub struct ApiDoc; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn openapi_spec_is_valid_and_has_expected_structure() { + let spec = ApiDoc::openapi(); + + assert_eq!(spec.info.title, "OxiCloud API"); + assert!(!spec.info.version.is_empty()); + + let paths = &spec.paths; + assert!( + paths.paths.len() >= 10, + "expected at least 10 paths, got {}", + paths.paths.len() + ); + assert!(paths.paths.contains_key("/api/trash"), "missing /api/trash"); + assert!( + paths.paths.contains_key("/api/shares"), + "missing /api/shares" + ); + assert!( + paths.paths.contains_key("/api/favorites"), + "missing /api/favorites" + ); + assert!( + paths.paths.contains_key("/api/recent"), + "missing /api/recent" + ); + + let schemas = &spec + .components + .as_ref() + .expect("components missing") + .schemas; + assert!( + schemas.len() >= 25, + "expected at least 25 schemas, got {}", + schemas.len() + ); + for name in [ + "FileDto", + "FolderDto", + "ShareDto", + "TrashedItemDto", + "UserDto", + ] { + assert!(schemas.contains_key(name), "missing schema: {name}"); + } + + let json = serde_json::to_string(&spec).expect("spec should serialise to JSON"); + assert!(json.len() > 1000, "spec JSON suspiciously small"); + } +} diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index 4ecd9625..f6edf151 100755 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -11,6 +11,7 @@ use std::sync::Arc; use tower_http::{compression::CompressionLayer, trace::TraceLayer}; use utoipa::OpenApi; +/// Returns the application version from Cargo.toml (compile-time constant) async fn get_version() -> AxumJson { AxumJson(json!({ "name": "OxiCloud",