# QDR - Temporary Shared Storage (via REST API) **Version: 1.0.0** A WordPress plugin that extends functionality for REST API interface to upload temporary media files and provide permalink to password-protected download pages. ## Description QDR Temporary Shared Storage enables secure sharing of large files through WordPress with complete control over access and expiration. This plugin is ideal for businesses that need to share sensitive documents, large media files, or private content with clients or team members for a limited time. ## Requirements - WordPress 6.8.1 or higher - PHP 8.2 or higher - WooCommerce 9.0 or higher (if WooCommerce integration is used) ## Features - **REST API Integration**: Upload and manage files programmatically - **Chunked File Uploads**: Support for uploading large files in chunks - **Password Protection**: Secure access to shared files - **Automatic Expiration**: Files automatically expire and are removed after their expiration date - **Download Tracking**: Track number of downloads and last download time - **Admin Interface**: Manage shared files through a user-friendly admin interface - **Custom Reference Codes**: Add reference codes for additional verification - **Storage Management**: Set limits on total storage used by shared files - **WordPress Media Library Integration**: Files are properly registered in WordPress media system ## Installation 1. Upload the `qdr-temporary-shared-storage` directory to the `/wp-content/plugins/` directory 2. Activate the plugin through the 'Plugins' menu in WordPress 3. Configure the plugin settings in 'Settings > Shared Storage' ## Usage ### Admin Interface - **Media > Shared Storage**: Manage all shared files - **Settings > Shared Storage**: Configure plugin settings ### Shortcode To display a download form for a specific file: ``` [qdr_tss_download id="123"] ``` To display a generic download form where users can enter an ID, password, and reference: ``` [qdr_tss_download] ``` ## REST API The plugin provides a complete REST API for programmatically uploading, managing, and downloading shared files. ### Authentication All API requests require an API key sent in the `X-Api-Key` HTTP header. You can generate or view your API key in the plugin settings page at "Settings > Shared Storage". ### API Endpoints #### Upload Endpoints - **POST /wp-json/qdr-tss/v1/upload/init**: Initialize chunked upload - **POST /wp-json/qdr-tss/v1/upload/chunk**: Upload a file chunk - **POST /wp-json/qdr-tss/v1/upload/finalize**: Finalize upload and create file record #### Media Endpoints - **GET /wp-json/qdr-tss/v1/media**: Get list of shared files - **GET /wp-json/qdr-tss/v1/media/{media_id}**: Get single file details - **PUT /wp-json/qdr-tss/v1/media/{media_id}**: Update file properties - **DELETE /wp-json/qdr-tss/v1/media/{media_id}**: Delete a file ### API Example (PHP) ```php // Initialize chunked upload $ch = curl_init('https://www.example.com/wp-json/qdr-tss/v1/upload/init'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: your-api-key-here' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'original_file_name' => 'example.pdf', 'file_size' => filesize('path/to/example.pdf') ]); $response = json_decode(curl_exec($ch)); curl_close($ch); $upload_id = $response->upload_id; // Upload chunks $file = fopen('path/to/example.pdf', 'rb'); $chunk_size = 1024 * 1024; // 1MB chunks $total_size = filesize('path/to/example.pdf'); $total_chunks = ceil($total_size / $chunk_size); for ($i = 0; $i < $total_chunks; $i++) { $chunk_data = fread($file, $chunk_size); // Upload the chunk // ... } // Finalize upload $ch = curl_init('https://www.example.com/wp-json/qdr-tss/v1/upload/finalize'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: your-api-key-here' ]); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'upload_id' => $upload_id, 'total_chunks' => $total_chunks, 'original_file_name' => 'example.pdf', 'description' => 'Example PDF file', 'message' => 'Please review this document', 'password' => 'secure_password', 'reference' => 'REF123', 'active_to' => date('Y-m-d H:i:s', strtotime('+30 days')) ]); $response = json_decode(curl_exec($ch)); curl_close($ch); // Get download link and media ID $media_id = $response->media_id; $permalink = $response->permalink; ``` ## REST API Documentation ### Authentication All API requests require an API key sent in the `X-Api-Key` HTTP header. You can generate or view your API key in the plugin settings page at "Settings > Shared Storage". ### API Base URL The base URL for all API endpoints is: ``` https://your-wordpress-site.com/wp-json/qdr-tss/v1/ ``` ### Endpoints #### 1. Initialize Chunked Upload Prepares the server for a chunked upload. - **Endpoint**: `/upload/init` - **Method**: `POST` - **Parameters**: - `original_file_name`: The original name of the file being uploaded - `file_size`: The total size of the file in bytes - **Response**: ```json { "upload_id": "5f8d7a6b9c3e", "status": "initialized" } ``` #### 2. Upload Chunk Uploads a single chunk of the file. - **Endpoint**: `/upload/chunk` - **Method**: `POST` - **Parameters**: - `upload_id`: The upload ID received from the initialization - `chunk_index`: The index of the current chunk (starting from 0) - `total_chunks`: The total number of chunks - `chunk`: The chunk file data (multipart/form-data) - **Response**: ```json { "upload_id": "5f8d7a6b9c3e", "chunk_index": 0, "status": "chunk_uploaded" } ``` #### 3. Finalize Upload Combines all chunks and creates the file record. - **Endpoint**: `/upload/finalize` - **Method**: `POST` - **Parameters**: - `upload_id`: The upload ID received from the initialization - `total_chunks`: The total number of chunks - `original_file_name`: The original name of the file - `description`: Description of the file - `message`: Custom message (optional) - `active_from`: Date from which permalink will be active (optional, defaults to current time) - `active_to`: Date until which permalink will be active (optional, defaults to 30 days from now) - `password`: Password for download protection - `reference`: Custom reference value (optional) - **Response**: ```json { "media_id": "1621234567", "permalink": "https://your-wordpress-site.com/shared-file-download/1621234567", "shortcode": "[qdr_tss_download id=\"1621234567\"]" } ``` #### 4. Get File Details Retrieves details about a specific file. - **Endpoint**: `/media/{media_id}` - **Method**: `GET` - **Response**: ```json { "id": 1, "media_id": "1621234567", "original_file_name": "example.pdf", "description": "Example document", "message": "Please review this document", "active_from": "2025-05-19 15:30:00", "active_to": "2025-06-18 15:30:00", "reference": "REF123", "upload_date": "2025-05-19 15:30:00", "download_count": 5, "last_download": "2025-05-20 10:45:30", "file_size": 1048576 } ``` #### 5. Update File Updates properties of a specific file. - **Endpoint**: `/media/{media_id}` - **Method**: `PUT` - **Parameters**: - `message`: Custom message (optional) - `active_from`: Date from which permalink will be active (optional) - `active_to`: Date until which permalink will be active (optional) - `reference`: Custom reference value (optional) - `description`: File description (optional) - **Response**: Updated file details (same format as Get File Details) #### 6. Delete File Deletes a specific file. - **Endpoint**: `/media/{media_id}` - **Method**: `DELETE` - **Response**: ```json { "deleted": true, "media_id": "1621234567" } ``` #### 7. List Files Retrieves a list of files with pagination and filtering. - **Endpoint**: `/media` - **Method**: `GET` - **Parameters**: - `page`: Page number (optional, default: 1) - `per_page`: Items per page (optional, default: 10) - `orderby`: Field to sort by (optional, default: "upload_date") - `order`: Sort order (optional, default: "DESC") - `original_file_name`: Filter by file name (optional) - `reference`: Filter by reference (optional) - **Response**: ```json [ { "id": 1, "media_id": "1621234567", "original_file_name": "example.pdf", "description": "Example document", "message": "Please review this document", "active_from": "2025-05-19 15:30:00", "active_to": "2025-06-18 15:30:00", "reference": "REF123", "upload_date": "2025-05-19 15:30:00", "download_count": 5, "last_download": "2025-05-20 10:45:30", "file_size": 1048576 }, ... ] ``` ### Code Examples #### PHP Example: Complete File Upload ```php $file_name, 'file_size' => $file_size ])); $response = curl_exec($ch); curl_close($ch); $init_result = json_decode($response, true); if (!isset($init_result['upload_id'])) { throw new Exception('Failed to initialize upload: ' . $response); } $upload_id = $init_result['upload_id']; // Step 2: Upload chunks $file = fopen($file_path, 'rb'); for ($i = 0; $i < $total_chunks; $i++) { // Create a temporary file for the chunk $chunk_file = tempnam(sys_get_temp_dir(), 'chunk'); $chunk_handle = fopen($chunk_file, 'wb'); // Read chunk from original file $chunk_data = fread($file, $chunk_size); fwrite($chunk_handle, $chunk_data); fclose($chunk_handle); // Upload the chunk $ch = curl_init($api_url . '/upload/chunk'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: ' . $api_key ]); $post_data = [ 'upload_id' => $upload_id, 'chunk_index' => $i, 'total_chunks' => $total_chunks, 'chunk' => new CURLFile($chunk_file, 'application/octet-stream', 'chunk') ]; curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); $response = curl_exec($ch); curl_close($ch); // Delete temporary chunk file unlink($chunk_file); $chunk_result = json_decode($response, true); if (!isset($chunk_result['status']) || $chunk_result['status'] !== 'chunk_uploaded') { throw new Exception('Failed to upload chunk ' . $i . ': ' . $response); } // Display progress echo "Uploaded chunk " . ($i + 1) . " of " . $total_chunks . "\n"; } fclose($file); // Step 3: Finalize upload $ch = curl_init($api_url . '/upload/finalize'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'X-Api-Key: ' . $api_key, 'Content-Type: application/json' ]); // Set expiration to 30 days from now $active_to = date('Y-m-d H:i:s', strtotime('+30 days')); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ 'upload_id' => $upload_id, 'total_chunks' => $total_chunks, 'original_file_name' => $file_name, 'description' => $description, 'message' => 'Uploaded via API example', 'password' => $password, 'reference' => $reference, 'active_to' => $active_to ])); $response = curl_exec($ch); curl_close($ch); $final_result = json_decode($response, true); if (!isset($final_result['media_id'])) { throw new Exception('Failed to finalize upload: ' . $response); } return $final_result; } ``` #### JavaScript Example: Chunked File Upload ```javascript /** * Uploads a file using chunked upload * @param {File} file The file to upload * @param {string} description File description * @param {string} password Password for download protection * @param {string} reference Optional reference * @param {function} progressCallback Callback for upload progress * @returns {Promise} Promise resolving to the upload result */ async function uploadSharedFile(file, description, password, reference = '', progressCallback = null) { const apiKey = 'your-api-key-here'; const apiUrl = 'https://your-wordpress-site.com/wp-json/qdr-tss/v1'; const chunkSize = 1024 * 1024; // 1MB chunks const totalChunks = Math.ceil(file.size / chunkSize); // Step 1: Initialize upload const initResponse = await fetch(`${apiUrl}/upload/init`, { method: 'POST', headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ original_file_name: file.name, file_size: file.size }) }); const initResult = await initResponse.json(); if (!initResult.upload_id) { throw new Error('Failed to initialize upload'); } const uploadId = initResult.upload_id; // Step 2: Upload chunks for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = Math.min(file.size, start + chunkSize); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('upload_id', uploadId); formData.append('chunk_index', i); formData.append('total_chunks', totalChunks); formData.append('chunk', chunk); const chunkResponse = await fetch(`${apiUrl}/upload/chunk`, { method: 'POST', headers: { 'X-Api-Key': apiKey }, body: formData }); const chunkResult = await chunkResponse.json(); if (!chunkResult.status || chunkResult.status !== 'chunk_uploaded') { throw new Error(`Failed to upload chunk ${i}`); } // Update progress if (progressCallback) { progressCallback((i + 1) / totalChunks * 100); } } // Step 3: Finalize upload const activeToDate = new Date(); activeToDate.setDate(activeToDate.getDate() + 30); // 30 days from now const activeTo = activeToDate.toISOString().slice(0, 19).replace('T', ' '); const finalizeResponse = await fetch(`${apiUrl}/upload/finalize`, { method: 'POST', headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ upload_id: uploadId, total_chunks: totalChunks, original_file_name: file.name, description: description, message: 'Uploaded via JavaScript API example', password: password, reference: reference, active_to: activeTo }) }); const finalResult = await finalizeResponse.json(); if (!finalResult.media_id) { throw new Error('Failed to finalize upload'); } return finalResult; } ``` ### Error Handling All API endpoints return standard HTTP status codes: - `200 OK`: Request succeeded - `201 Created`: Resource created successfully - `400 Bad Request`: Invalid request parameters - `401 Unauthorized`: Missing API key - `403 Forbidden`: Invalid API key - `404 Not Found`: Resource not found - `500 Internal Server Error`: Server error Error responses include a message explaining the error: ```json { "code": "qdr_tss_not_found", "message": "Media not found.", "data": { "status": 404 } } ``` ### Best Practices 1. Always use chunked uploads for large files 2. Set reasonable expiration dates for files to avoid cluttering storage 3. Use unique reference codes to help identify files 4. Regularly check and purge expired files using the admin interface 5. Include descriptive file names and messages for better user experience ## Configuration Options ### API Key Generate or specify an API key for REST API authentication. ### Media Category Specify a category to assign to uploaded media files. ### Maximum Storage Size Set the maximum total storage size for all shared files. ## Security Considerations - All shared files are password-protected - Files are stored in a protected directory that cannot be accessed directly - Automatic expiration prevents unauthorized access to outdated files - API authentication through an API key ensures only authorized applications can upload files ## License GPL v2 or later - https://www.gnu.org/licenses/gpl-2.0.html ## Author [Dalibor Votruba](https://www.quadarax.com)