forked from s60sc/ESP32-CAM_MJPEG2SD
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ftp.cpp
388 lines (352 loc) · 13.3 KB
/
ftp.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
// Upload SD card or SPIFFS content to a remote server using FTP or HTTPS
//
// s60sc 2022, 2023
#include "appGlobals.h"
#if INCLUDE_FTP_HFS
#if (!INCLUDE_CERTS)
const char* hfs_rootCACertificate = "";
const char* ftps_rootCACertificate = "";
#endif
// File server params (FTP or HTTPS), setup via web page
char fsServer[MAX_HOST_LEN];
uint16_t fsPort = 21;
char FS_Pass[MAX_PWD_LEN]; // FTP password or HTTPS passcode
char fsWd[FILE_NAME_LEN];
static bool uploadInProgress = false;
uint8_t percentLoaded = 0;
TaskHandle_t fsHandle = NULL;
static char storedPathName[FILE_NAME_LEN];
static char folderPath[FILE_NAME_LEN];
static byte* fsChunk;
bool deleteAfter = false; // auto delete after upload
bool autoUpload = false; // Automatically upload every created file to remote file server
bool fsUse = false; // FTP if false, HTTPS if true
/******************** HTTPS ********************/
// Upload file of folder of files from local storage to remote HTTPS file server
// Requires significant heap space due to TLS.
// Each file POST has following format, where the following values are derived
// from the web page:
// Host: FS Server
// port: FS port
// passcode: FS password
// pathname: FS root dir + selected day folder/file
/*
POST /upload HTTP/1.1
Host: 192.168.1.135
Content-Length: 2412358
Content-Type: multipart/form-data; boundary=123456789000000000000987654321
--123456789000000000000987654321
Content-disposition: form-data; name="json"
Content-Type: "application/json"
{"pathname":"/FS/root/dir/20231119/20231119_140513_SVGA_20_6_120.avi","passcode":"abcd1234"}
--123456789000000000000987654321
Content-disposition: form-data; name="file"; filename="20231119_140513_SVGA_20_6_120.avi"
Content-Type: "application/octet-stream"
<file content>
--123456789000000000000987654321
*/
#define CONTENT_TYPE "Content-Type: \"%s\"\r\n\r\n"
#define POST_HDR "POST /%s HTTP/1.1\r\nHost: %s\r\nContent-Length: %u\r\n" CONTENT_TYPE
#define MULTI_TYPE "multipart/form-data; boundary=" BOUNDARY_VAL
#define JSON_TYPE "application/json"
#define BIN_TYPE "application/octet-stream"
#define FORM_DATA "--" BOUNDARY_VAL "\r\nContent-disposition: form-data; name=\"%s%s\"\r\n" CONTENT_TYPE
#define END_BOUNDARY "\r\n--" BOUNDARY_VAL "--\r\n"
#define FILE_NAME "file\"; filename=\""
#define JSON_DATA "{\"pathname\":\"%s%s/%s\",\"passcode\":\"%s\"}"
#define FORM_OFFSET 256 // offset in fsBuff to prepare form data
NetworkClientSecure hclient;
char* fsBuff;
static void postHeader(const char* tmethod, const char* contentType, bool isFile,
size_t fileSize, const char* fileName) {
// create http post header
char* p = fsBuff + FORM_OFFSET; // leave space for http request data
if (isFile) {
p += sprintf(p, FORM_DATA, "json", "", JSON_TYPE);
// fsBuff initially contains folder name
p += sprintf(p, JSON_DATA, fsWd, folderPath, fileName, FS_Pass);
p += sprintf(p, "\r\n" FORM_DATA, FILE_NAME, fileName, BIN_TYPE);
} // else JSON data already loaded by hfsCreateFolder()
size_t formLen = strlen(fsBuff + FORM_OFFSET);
// create http request header
p = fsBuff;
if (isFile) fileSize += formLen + strlen(END_BOUNDARY);
p += sprintf(p, POST_HDR, tmethod, fsServer, fileSize, isFile ? MULTI_TYPE : JSON_TYPE);
size_t reqLen = strlen(fsBuff);
// concatenate request and form data
if (formLen) {
memmove(fsChunk + reqLen, fsChunk + FORM_OFFSET, formLen);
fsChunk[reqLen + formLen] = 0;
}
hclient.print(fsBuff); // http header
}
static bool hfsStoreFile(File &fh) {
// Upload individual file to HTTPS server
// reject if folder or not valid file type
#ifdef ISCAM
if (!strstr(fh.name(), AVI_EXT) && !strstr(fh.name(), CSV_EXT) && !strstr(fh.name(), SRT_EXT)) return false;
#else
if (!strstr(fh.name(), FILE_EXT)) return false;
#endif
LOG_INF("Upload file: %s, size: %s", fh.name(), fmtSize(fh.size()));
// prep POST header and send file to HTTPS server
postHeader("upload", BIN_TYPE, true, fh.size(), fh.name());
// upload file content in chunks
uint8_t percentLoaded = 0;
size_t chunksize = 0, totalSent = 0;
while ((chunksize = fh.read((uint8_t*)fsChunk, CHUNKSIZE))) {
hclient.write((uint8_t*)fsChunk, chunksize);
totalSent += chunksize;
if (calcProgress(totalSent, fh.size(), 5, percentLoaded)) LOG_INF("Uploaded %u%%", percentLoaded);
}
percentLoaded = 100;
hclient.println(END_BOUNDARY);
return true;
}
/******************** FTP ********************/
// FTP control
bool useFtps = false;
char ftpUser[MAX_HOST_LEN];
static char rspBuf[256]; // Ftp response buffer
static char respCodeRx[4]; // ftp response code
static fs::FS fp = STORAGE;
#define NO_CHECK "999"
// WiFi Clients
NetworkClient rclient;
NetworkClient dclient;
static bool sendFtpCommand(const char* cmd, const char* param, const char* respCode, const char* respCode2 = NO_CHECK) {
// build and send ftp command
if (strlen(cmd)) {
rclient.print(cmd);
rclient.println(param);
}
LOG_VRB("Sent cmd: %s%s", cmd, param);
// wait for ftp server response
uint32_t start = millis();
while (!rclient.available() && millis() < start + (responseTimeoutSecs * 1000)) delay(1);
if (!rclient.available()) {
LOG_WRN("FTP server response timeout");
return false;
}
// read in response code and message
rclient.read((uint8_t*)respCodeRx, 3);
respCodeRx[3] = 0; // terminator
int readLen = rclient.read((uint8_t*)rspBuf, 255);
rspBuf[readLen] = 0;
while (rclient.available()) rclient.read(); // bin the rest of response
// check response code with expected
LOG_VRB("Rx code: %s, resp: %s", respCodeRx, rspBuf);
if (strcmp(respCode, NO_CHECK) == 0) return true; // response code not checked
if (strcmp(respCodeRx, respCode) != 0) {
if (strcmp(respCodeRx, respCode2) != 0) {
// incorrect response code
LOG_ERR("Command %s got wrong response: %s %s", cmd, respCodeRx, rspBuf);
return false;
}
}
return true;
}
static bool ftpConnect() {
// Connect to ftp or ftps
if (rclient.connect(fsServer, fsPort)) {LOG_VRB("FTP connected at %s:%u", fsServer, fsPort);}
else {
LOG_WRN("Error opening ftp connection to %s:%u", fsServer, fsPort);
return false;
}
if (!sendFtpCommand("", "", "220")) return false;
if (useFtps) {
if (sendFtpCommand("AUTH ", "TLS", "234")) {
/* NOT IMPLEMENTED */
} else LOG_WRN("FTPS not available");
}
if (!sendFtpCommand("USER ", ftpUser, "331")) return false;
if (!sendFtpCommand("PASS ", FS_Pass, "230")) return false;
// change to supplied folder
if (!sendFtpCommand("CWD ", fsWd, "250")) return false;
if (!sendFtpCommand("Type I", "", "200")) return false;
return true;
}
static void ftpDisconnect() {
// Disconnect from ftp server
rclient.println("QUIT");
dclient.stop();
rclient.stop();
}
static bool ftpCreateFolder(const char* folderName) {
// create folder if non existent then change to it
LOG_VRB("Check for folder %s", folderName);
sendFtpCommand("CWD ", folderName, NO_CHECK);
if (strcmp(respCodeRx, "550") == 0) {
// non existent folder, create it
if (!sendFtpCommand("MKD ", folderName, "257")) return false;
//sendFtpCommand("SITE CHMOD 755 ", folderName, "200", "550"); // unix only
if (!sendFtpCommand("CWD ", folderName, "250")) return false;
}
return true;
}
static bool openDataPort() {
// set up port for data transfer
if (!sendFtpCommand("PASV", "", "227")) return false;
// derive data port number
char* p = strchr(rspBuf, '('); // skip over initial text
int p1, p2;
int items = sscanf(p, "(%*d,%*d,%*d,%*d,%d,%d)", &p1, &p2);
if (items != 2) {
LOG_ERR("Failed to parse data port");
return false;
}
int dataPort = (p1 << 8) + p2;
// Connect to data port
LOG_VRB("Data port: %i", dataPort);
if (!dclient.connect(fsServer, dataPort)) {
LOG_WRN("Data connection failed");
return false;
}
return true;
}
static bool ftpStoreFile(File &fh) {
// Upload individual file to current folder, overwrite any existing file
// reject if folder, or not valid file type
#ifdef ISCAM
if (!strstr(fh.name(), AVI_EXT) && !strstr(fh.name(), CSV_EXT) && !strstr(fh.name(), SRT_EXT)) return false;
#else
if (!strstr(fh.name(), FILE_EXT)) return false;
#endif
char ftpSaveName[FILE_NAME_LEN];
strcpy(ftpSaveName, fh.name());
size_t fileSize = fh.size();
LOG_INF("Upload file: %s, size: %s", ftpSaveName, fmtSize(fileSize));
// open data connection
openDataPort();
uint32_t writeBytes = 0;
uint32_t uploadStart = millis();
size_t readLen, writeLen;
if (!sendFtpCommand("STOR ", ftpSaveName, "150", "125")) return false;
do {
// upload file in chunks
readLen = fh.read(fsChunk, CHUNKSIZE);
if (readLen) {
writeLen = dclient.write((const uint8_t*)fsChunk, readLen);
writeBytes += writeLen;
if (writeLen == 0) {
LOG_WRN("Upload file to ftp failed");
return false;
}
if (calcProgress(writeBytes, fileSize, 5, percentLoaded)) LOG_INF("Uploaded %u%%", percentLoaded);
}
} while (readLen > 0);
dclient.stop();
percentLoaded = 100;
bool res = sendFtpCommand("", "", "226");
if (res) {
LOG_ALT("Uploaded %s in %u sec", fmtSize(writeBytes), (millis() - uploadStart) / 1000);
//sendFtpCommand("SITE CHMOD 644 ", ftpSaveName, "200", "550"); // unix only
} else LOG_WRN("File transfer not successful");
return res;
}
/******************** Common ********************/
static bool getFolderName(const char* folderName) {
// extract folder names from path name
strcpy(folderPath, folderName);
int pos = 1; // skip 1st '/'
// get each folder name in sequence
bool res = true;
for (char* p = strchr(folderPath, '/'); (p = strchr(++p, '/')) != NULL; pos = p + 1 - folderPath) {
*p = 0; // terminator
if (!fsUse) res = ftpCreateFolder(folderPath + pos);
}
return res;
}
static bool uploadFolderOrFileFs(const char* fileOrFolder) {
// Upload a single file or whole folder using FTP or HTTPS server
// folder is uploaded file by file
fsBuff = (char*)fsChunk;
bool res = fsUse ? remoteServerConnect(hclient, fsServer, fsPort, hfs_rootCACertificate, FSFTP) : ftpConnect();
if (!res) {
LOG_WRN("Unable to connect to %s server", fsUse ? "HTTPS" : "FTP");
return false;
}
res = false;
const int saveRefreshVal = refreshVal;
refreshVal = 1;
File root = fp.open(fileOrFolder);
if (!root.isDirectory()) {
// Upload a single file
char fsSaveName[FILE_NAME_LEN];
strcpy(fsSaveName, root.path());
if (getFolderName(root.path())) res = fsUse ? hfsStoreFile(root) : ftpStoreFile(root);
#ifdef ISCAM
// upload corresponding csv and srt files if exist
if (res) {
changeExtension(fsSaveName, CSV_EXT);
if (fp.exists(fsSaveName)) {
File csv = fp.open(fsSaveName);
res = fsUse ? hfsStoreFile(csv) : ftpStoreFile(csv);
csv.close();
}
changeExtension(fsSaveName, SRT_EXT);
if (fp.exists(fsSaveName)) {
File srt = fp.open(fsSaveName);
res = fsUse ? hfsStoreFile(srt) : ftpStoreFile(srt);
srt.close();
}
}
if (!res) LOG_WRN("Failed to upload: %s", fsSaveName);
#endif
} else {
// Upload a whole folder, file by file
LOG_INF("Uploading folder: ", root.name());
strncpy(folderPath, root.name(), FILE_NAME_LEN - 1);
res = fsUse ? true : ftpCreateFolder(root.name());
if (!res) {
refreshVal = saveRefreshVal;
return false;
}
File fh = root.openNextFile();
while (fh) {
res = fsUse ? hfsStoreFile(fh) : ftpStoreFile(fh);
if (!res) break; // abandon rest of files
fh.close();
fh = root.openNextFile();
}
if (fh) fh.close();
}
refreshVal = saveRefreshVal;
root.close();
fsUse ? remoteServerClose(hclient) : ftpDisconnect();
return res;
}
static void fileServerTask(void* parameter) {
// process an FTP or HTTPS request
#ifdef ISCAM
doPlayback = false; // close any current playback
#endif
fsChunk = psramFound() ? (byte*)ps_malloc(CHUNKSIZE) : (byte*)malloc(CHUNKSIZE);
if (strlen(storedPathName) >= 2) {
File root = fp.open(storedPathName);
if (!root) LOG_WRN("Failed to open: %s", storedPathName);
else {
bool res = uploadFolderOrFileFs(storedPathName);
if (res && deleteAfter) deleteFolderOrFile(storedPathName);
}
} else LOG_VRB("Root or null is not allowed %s", storedPathName);
uploadInProgress = false;
free(fsChunk);
fsHandle = NULL;
vTaskDelete(NULL);
}
bool fsStartTransfer(const char* fileFolder) {
// called from other functions to commence transfer of file or folder to file server
setFolderName(fileFolder, storedPathName);
if (!uploadInProgress) {
uploadInProgress = true;
if (fsHandle == NULL) xTaskCreate(&fileServerTask, "fileServerTask", FS_STACK_SIZE, NULL, FTP_PRI, &fsHandle);
debugMemory("fsStartTransfer");
return true;
} else LOG_WRN("Unable to transfer %s as another transfer in progress", storedPathName);
return false;
}
void prepUpload() {
LOG_INF("File uploads will use %s server", fsUse ? "HTTPS" : "FTP");
}
#endif