ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
upload_service.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file upload_service.cpp
6 * @brief Streaming file upload: POST body -> Arduino FS file (PC_ENABLE_UPLOAD).
7 */
8
9#include "upload_service.h"
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_UPLOAD
13
15#include "protocore.h"
17#include <stdio.h>
18#include <string.h>
19
20// All upload-service state, owned by one instance (internal linkage): the server handle, the
21// route path, the destination filesystem + path, and the per-upload file/flags/counter (one
22// upload at a time on this single-task device). Grouped so it is one named owner, cross-TU
23// unreachable.
24struct UploadCtx
25{
26 PC *server = nullptr;
27 const char *path = nullptr;
28 fs::FS *fs = nullptr;
29 const char *dest = nullptr;
30 fs::File file;
31 bool active = false; ///< Destination file opened for the current upload.
32 bool error = false; ///< A write failed during the current upload.
33 size_t written = 0; ///< Bytes written so far / in the last upload.
34};
35static UploadCtx s_upl;
36
37/// @brief Stream-begin hook: accept POST @p s_upl.path and open the destination file.
38static bool upload_stream_begin(HttpReq *req)
39{
40 if (strcmp(req->method, "POST") != 0)
41 {
42 return false;
43 }
44 // The `!s_upl.path` half is unreachable: s_upl.path is only ever set by pc_upload_begin(),
45 // which immediately hands that same pointer to server.on() -> fill_route_base(), whose
46 // strncpy() would fault on a null path before any request could reach this hook. So by the
47 // time this hook is installed, s_upl.path is always a live C string.
48 if (!s_upl.path || strcmp(req->path, s_upl.path) != 0) // GCOVR_EXCL_BR_LINE null path unreachable (see above)
49 {
50 return false;
51 }
52
53 s_upl.active = false;
54 s_upl.error = false;
55 s_upl.written = 0;
56 // The `s_upl.fs` half is unreachable: it is always the address of the `fs::FS &fs` reference
57 // parameter of pc_upload_begin(), and a reference can't be null without UB at the call site -
58 // only s_upl.dest (a plain pointer) can legitimately be null here.
59 if (s_upl.fs && s_upl.dest) // GCOVR_EXCL_BR_LINE null fs unreachable (see above)
60 {
61 s_upl.file = s_upl.fs->open(s_upl.dest, "w");
62 if (s_upl.file)
63 {
64 s_upl.active = true;
65 }
66 else
67 {
68 s_upl.error = true;
69 }
70 }
71 // Stream regardless so the body is consumed and the route handler can reply.
72 return true;
73}
74
75/// @brief Stream-data hook: write one body chunk to the destination file.
76static void upload_stream_data(HttpReq *req, const uint8_t *data, size_t len)
77{
78 (void)req; // a single upload streams at a time
79 if (s_upl.active && !s_upl.error)
80 {
81 if (s_upl.file.write(data, len) != len)
82 {
83 s_upl.error = true;
84 }
85 else
86 {
87 s_upl.written += len;
88 }
89 }
90}
91
92/// @brief Route handler (runs at ParseState::PARSE_COMPLETE): close the file and reply.
93static void upload_handle(uint8_t slot_id, HttpReq *req)
94{
95 if (!req->body_streaming)
96 {
97 s_upl.server->send(slot_id, 400, PC_MIME_TEXT_PLAIN, "POST a file body");
98 return;
99 }
100 if (s_upl.active)
101 {
102 s_upl.file.close();
103 }
104 if (!s_upl.active || s_upl.error)
105 {
106 s_upl.server->send(slot_id, 500, PC_MIME_TEXT_PLAIN, "upload failed");
107 return;
108 }
109 char msg[48];
110 pc_sb sb_msg = {msg, sizeof(msg), 0, true};
111 pc_sb_put(&sb_msg, "OK ");
112 pc_sb_u32(&sb_msg, (uint32_t)((unsigned)s_upl.written));
113 pc_sb_put(&sb_msg, " bytes");
114 if (pc_sb_finish(&sb_msg) == 0)
115 {
116 msg[0] = '\0';
117 }
118 s_upl.server->send(slot_id, 200, PC_MIME_TEXT_PLAIN, msg);
119}
120
121size_t pc_upload_last_size()
122{
123 return s_upl.written;
124}
125
126void pc_upload_begin(PC &server, const char *path, fs::FS &fs, const char *dest_path)
127{
128 s_upl.server = &server;
129 s_upl.path = path;
130 s_upl.fs = &fs;
131 s_upl.dest = dest_path;
132
133 http_parser_set_stream_hooks(upload_stream_begin, upload_stream_data);
134 server.on(path, HttpMethod::HTTP_POST, upload_handle);
135}
136
137#endif // PC_ENABLE_UPLOAD
Single-port HTTP server with deterministic, zero-allocation execution.
Definition protocore.h:348
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
Standalone HTTP/1.1 request parser - no transport dependency.
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
Layer 7 (Application) - public HTTP routing API.
@ HTTP_POST
Non-idempotent create / action.
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
Fully-parsed HTTP/1.1 request.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
char method[PC_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
Streaming file upload to an Arduino FS (PC_ENABLE_UPLOAD).