SaveFileHandler Documentation
Overview
The SaveFileHandler class is an HTTP handler that enables authenticated users to save or
update file contents in their website's file structure. It provides a secure API endpoint
for writing text-based file content, typically used by the web-based code editor to persist
changes made to HTML, CSS, JavaScript, PHP, and other text files.
Purpose
This handler is part of the multi-tenant hosting service's file management system, allowing
users to edit and save files through the web interface. It handles both creating new files
and updating existing ones, making it a critical component of the online editing workflow.
Core Functionality
This handler performs the following key operations:
1. Request Validation
- Only accepts POST requests
- Silently ignores non-POST methods (no response sent)
- Expects JSON payload containing file path, filename, and content
2. Authentication & Authorization
- Extracts and validates the session ID from the request via getJavaSessionId()
- Verifies the session through SessionManager.getUsername()
- Rejects requests with invalid or expired sessions (HTTP 403)
- Ensures users can only save files within their own directory space
3. Content Normalization
- Extracts the file content from the JSON payload
- Normalizes line endings by converting Windows-style CRLF (\r\n) to Unix-style LF (\n)
- Ensures consistent line ending format across different operating systems
4. Path Construction
The handler builds the target file path based on the authenticated user:
- Standard users: {USER_DIR}/{username}/static/{path}{filename}
- Administrator (ADMIN_HOST match): {ADMIN_DIR}/static/{path}{filename}
Where:
- USER_DIR is the base directory for user accounts (likely /home/lukas/users/)
- ADMIN_DIR is the special directory for administrative content (likely /home/lukas/JavaServerProject/www)
- {path} is the optional subdirectory path provided in the JSON request body
- {filename} is the name of the file to save, provided in the JSON request body
5. File Writing
- Delegates file writing to the writeFile() helper method
- writeFile() returns a boolean indicating success or failure
- Creates or overwrites the file at the specified path with the provided content
6. Response Messages
Returns different responses based on the operation result:
- "Success" (200): File was successfully written to disk
- 403 Forbidden: File write operation failed or session is invalid
Request Format
Method: POST
Content-Type: application/json
Body:
{
"path": "relative/directory/path/",
"filename": "file.ext",
"content": "The complete file content as a string"
}
The path value should be relative to the user's static directory and may be
empty for files in the root. The filename is the name of the file to save.
The content is the complete text content to write to the file.
Response Format
Success Response:
- Status: 200 OK
- Content-Type: text/plain; charset=UTF-8
- Body: "Success"
Error Response:
- Status: 403 Forbidden
- Body: Empty (response body closed immediately)
Response Codes
- 200 OK: File was successfully saved
- 403 Forbidden: Invalid session or file write failed
- No response: Non-POST request (handler returns without sending response)
Security Features
- Session-based authentication: All requests require valid session credentials
- User isolation: Users can only save files within their own directory structure
- Path scoping: File paths are always constructed relative to the user's base directory, preventing directory traversal attacks
- Administrator separation: Special handling for admin accounts with different base paths
- Content validation: Line ending normalization prevents potential issues with different OS formats
Error Handling
- Invalid session: Logs "Rejected: session not valid" and returns 403 Forbidden
- Write failure: Returns 403 Forbidden if writeFile() returns false
- Non-POST request: Handler returns silently without sending any response
- IOException: Propagates to calling code (may result in 500 error)
Logging
The handler provides console logging for debugging:
- Logs "Rejected: session not valid" when authentication fails
Use Cases
This handler is typically invoked when users:
- Save changes made in the web-based code editor
- Create new HTML, CSS, JavaScript, or PHP files
- Update configuration files or data files
- Modify existing website content through the editor
- Write or update text-based files of any type
Dependencies
This handler relies on:
- parseJsonToMap(): Converts JSON request body to a key-value map
- getJavaSessionId(): Extracts session identifier from the HTTP request
- SessionManager.getUsername(): Validates session and retrieves the authenticated username
- writeFile(): Helper method that performs the actual file writing operation
- USER_DIR: Constant defining the base directory for user accounts
- ADMIN_DIR: Constant defining the base directory for administrative content
- ADMIN_HOST: Constant identifying the administrator username
Behavior Notes
- Always normalizes line endings from CRLF (\r\n) to LF (\n) before saving
- Creates new files or completely overwrites existing files (no append mode)
- Uses 403 Forbidden for write failures instead of more specific error codes
- Non-POST requests are ignored silently without any response, which may cause client timeouts
- Path parameter can be empty or null for files in the root static directory
- The entire file content must be sent in a single request (no streaming or chunking)
Line Ending Normalization
The handler automatically converts Windows-style line endings (CRLF: \r\n) to Unix-style
line endings (LF: \n). This ensures:
- Consistent file format regardless of client operating system
- Better compatibility with Unix/Linux server environments
- Smaller file sizes (fewer bytes per line ending)
- Consistent behavior when files are edited from different platforms
Performance Considerations
- Memory usage: The entire file content is loaded into memory as a string
- No streaming: Large files must be sent and processed in their entirety
- String operations: Line ending replacement operates on the complete content string
- No compression: Content is transmitted as plain text without compression
Potential Improvements
- Return more specific error codes (404 for path not found, 500 for I/O errors)
- Send HTTP 405 Method Not Allowed for non-POST requests instead of silent failure
- Add file size limits to prevent memory issues with very large files
- Implement automatic backup before overwriting existing files
- Add validation for file types and extensions
- Provide more detailed error messages about why writes fail
- Support streaming for large file writes
- Add transaction support or atomic writes for reliability
- Implement version control or file history
- Add optional line ending preservation (don't always normalize)
flowchart TD
A(["Start handle"]) --> B{"Request method
== POST?"}
B -->|No| C["Return silently
(No response sent)"]
B -->|Yes| D["Read request body
with UTF-8 encoding"]
C --> Z(["End"])
D --> E["Get sessionId via
getJavaSessionId(exchange)"]
E --> F["Get username from
SessionManager.getUsername(sessionId)"]
F --> G{"Username
is null?"}
G -->|Yes| H["Log: 'Rejected: session not valid'
Send 403 Forbidden
Close response body"]
G -->|No| I["Parse JSON body
using parseJsonToMap"]
H --> Z
I --> J["Extract content from map
Normalize line endings:
content = map.get('content').replace('\\r\\n', '\\n')"]
J --> K["Build base userPath:
USER_DIR + user + /static/"]
K --> L{"Username ==
ADMIN_HOST?"}
L -->|Yes| M["Override userPath:
ADMIN_DIR + /static/"]
L -->|No| N["Keep user base path"]
M --> O
N --> O
O{"map.get(path) not
empty and not null?"}
O -->|Yes| P["Append path to userPath"]
O -->|No| Q["Keep userPath as is"]
P --> R["Append filename:
userPath += map.get('filename')"]
Q --> R
R --> S["Call writeFile(userPath, content)"]
S --> T{"writeFile returned
true (success)?"}
T -->|Yes| U["Set msg = 'Success'
Convert to bytes"]
T -->|No| V["Send 403 Forbidden
Close response body"]
V --> Z
U --> W["Set Content-Type header:
text/plain; charset=UTF-8"]
W --> X["Send 200 response
with response.length"]
X --> Y1["Write response bytes
to OutputStream"]
Y1 --> Y2["Close OutputStream"]
Y2 --> Z