Download
jcs
/subtext
/folder.c
(View History)
jcs folder: constify fields | Latest amendment: 276 on 2022-11-11 |
1 | /* |
2 | * Copyright (c) 2022 joshua stein <jcs@jcs.org> |
3 | * |
4 | * Permission to use, copy, modify, and distribute this software for any |
5 | * purpose with or without fee is hereby granted, provided that the above |
6 | * copyright notice and this permission notice appear in all copies. |
7 | * |
8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
15 | */ |
16 | |
17 | #include <stdarg.h> |
18 | #include <stdio.h> |
19 | #include <stdlib.h> |
20 | #include <string.h> |
21 | |
22 | #include "subtext.h" |
23 | #include "ansi.h" |
24 | #include "folder.h" |
25 | #include "sha1.h" |
26 | #include "user.h" |
27 | #include "zmodem.h" |
28 | |
29 | #define FILES_PER_PAGE 10 |
30 | |
31 | #define FILE_VIEW_RETURN_DONE -1 |
32 | #define FILE_VIEW_RETURN_LIST -2 |
33 | #define FILE_VIEW_RETURN_FIND -3 |
34 | |
35 | const struct struct_field folder_fields[] = { |
36 | { "Folder ID", CONFIG_TYPE_LONG, |
37 | offsetof(struct folder, id), |
38 | 1, ULONG_MAX }, |
39 | { "Name", CONFIG_TYPE_STRING, |
40 | offsetof(struct folder, name), |
41 | 1, member_size(struct folder, name) }, |
42 | { "Disk Path", CONFIG_TYPE_STRING, |
43 | offsetof(struct folder, path), |
44 | 1, member_size(struct folder, path) }, |
45 | { "Description", CONFIG_TYPE_STRING, |
46 | offsetof(struct folder, description), |
47 | 0, member_size(struct folder, description) }, |
48 | { "Restricted Posting", CONFIG_TYPE_BOOLEAN, |
49 | offsetof(struct folder, restricted_posting), |
50 | 0, 0 }, |
51 | { "Restricted Viewing", CONFIG_TYPE_BOOLEAN, |
52 | offsetof(struct folder, restricted_viewing), |
53 | 0, 0 }, |
54 | }; |
55 | const size_t nfolder_fields = nitems(folder_fields); |
56 | |
57 | const struct bile_object_field folder_object_fields[] = { |
58 | { offsetof(struct folder, id), |
59 | member_size(struct folder, id), -1 }, |
60 | { offsetof(struct folder, name), |
61 | member_size(struct folder, name), -1 }, |
62 | { offsetof(struct folder, description), |
63 | member_size(struct folder, description), -1 }, |
64 | { offsetof(struct folder, restricted_posting), |
65 | member_size(struct folder, restricted_posting), -1 }, |
66 | { offsetof(struct folder, restricted_viewing), |
67 | member_size(struct folder, restricted_viewing), -1 }, |
68 | { offsetof(struct folder, last_upload_at), |
69 | member_size(struct folder, last_upload_at), -1 }, |
70 | { offsetof(struct folder, file_count), |
71 | member_size(struct folder, file_count), -1 }, |
72 | { offsetof(struct folder, path), |
73 | member_size(struct folder, path), -1 }, |
74 | }; |
75 | const size_t nfolder_object_fields = nitems(folder_object_fields); |
76 | |
77 | const struct bile_object_field folder_file_object_fields[] = { |
78 | { offsetof(struct folder_file, id), |
79 | member_size(struct folder_file, id), -1 }, |
80 | { offsetof(struct folder_file, time), |
81 | member_size(struct folder_file, time), -1 }, |
82 | { offsetof(struct folder_file, uploader_user_id), |
83 | member_size(struct folder_file, uploader_user_id), -1 }, |
84 | { offsetof(struct folder_file, filename), |
85 | member_size(struct folder_file, filename), -1 }, |
86 | { offsetof(struct folder_file, description), |
87 | member_size(struct folder_file, description), -1 }, |
88 | { offsetof(struct folder_file, size), |
89 | member_size(struct folder_file, size), -1 }, |
90 | { offsetof(struct folder_file, sha1_checksum), |
91 | member_size(struct folder_file, sha1_checksum), -1 }, |
92 | { offsetof(struct folder_file, notes_size), |
93 | member_size(struct folder_file, notes_size), -1 }, |
94 | { offsetof(struct folder_file, notes), |
95 | -1, offsetof(struct folder_file, notes_size) }, |
96 | }; |
97 | const size_t nfolder_file_object_fields = nitems(folder_file_object_fields); |
98 | |
99 | unsigned long folder_upload(struct session *s, struct folder *folder, |
100 | char *initial_filename, char *initial_description); |
101 | void folder_list_files(struct session *s, struct folder *folder, |
102 | size_t nfile_ids, unsigned long *file_ids, size_t page, size_t pages); |
103 | void folder_show(struct session *s, struct folder *folder); |
104 | short folder_file_view(struct session *s, struct folder *folder, |
105 | unsigned long id, short idx); |
106 | void folder_delete_file(struct session *s, struct folder *folder, |
107 | struct folder_file *file); |
108 | size_t folder_find_file_ids(struct folder *folder, size_t *nfile_ids, |
109 | unsigned long **file_ids); |
110 | bool folder_file_save(struct folder *folder, struct folder_file *file, |
111 | char *temp_path); |
112 | bool folder_file_valid_filename(struct session *session, |
113 | struct folder *folder, struct folder_file *file, char **error); |
114 | void folder_edit_file(struct session *s, struct folder *folder, |
115 | struct folder_file *file); |
116 | |
117 | void |
118 | folder_list(struct session *s) |
119 | { |
120 | static const struct session_menu_option opts[] = { |
121 | { '#', "#0123456789", "View file folder [#]" }, |
122 | { 'l', "Ll", "List folders" }, |
123 | { 'q', "QqXx", "Return to main menu" }, |
124 | { '?', "?", "List menu options" }, |
125 | }; |
126 | short n; |
127 | char c; |
128 | bool done, show_list, show_help; |
129 | |
130 | show_list = true; |
131 | show_help = false; |
132 | done = false; |
133 | |
134 | while (!done && !s->ending) { |
135 | if (show_list) { |
136 | session_printf(s, "{{B}}File Folders{{/B}}\r\n"); |
137 | session_printf(s, "%s# Name Description%s\r\n", |
138 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
139 | session_flush(s); |
140 | |
141 | for (n = 0; n < db->nfolders; n++) { |
142 | session_printf(s, "%d %- 13.13s %s\r\n", |
143 | n, |
144 | db->folders[n].name, |
145 | db->folders[n].description); |
146 | } |
147 | session_flush(s); |
148 | |
149 | show_list = false; |
150 | } |
151 | |
152 | c = session_menu(s, "File Folders", "Files", opts, nitems(opts), |
153 | show_help); |
154 | show_help = false; |
155 | |
156 | handle_opt: |
157 | switch (c) { |
158 | case 'l': |
159 | show_list = true; |
160 | break; |
161 | case 0: |
162 | case 1: |
163 | case 2: |
164 | case 3: |
165 | case 4: |
166 | case 5: |
167 | case 6: |
168 | case 7: |
169 | case 8: |
170 | case 9: |
171 | if (c >= db->nfolders) { |
172 | session_printf(s, "Invalid folder\r\n"); |
173 | session_flush(s); |
174 | break; |
175 | } |
176 | folder_show(s, &db->folders[c]); |
177 | break; |
178 | case '?': |
179 | show_help = true; |
180 | break; |
181 | default: |
182 | done = true; |
183 | break; |
184 | } |
185 | } |
186 | } |
187 | |
188 | void |
189 | folder_show(struct session *s, struct folder *folder) |
190 | { |
191 | static const struct session_menu_option opts[] = { |
192 | { '#', "#0123456789", "View file [#]" }, |
193 | { '<', "<", "Previous page of files" }, |
194 | { 'l', "Ll", "List files" }, |
195 | { '>', ">", "Next page of files" }, |
196 | { 'u', "Uu", "Upload new file" }, |
197 | { 'q', "QqXx", "Return to main menu" }, |
198 | { '?', "?", "List menu options" }, |
199 | }; |
200 | char prompt[6 + BOARD_NAME_LENGTH + 8]; |
201 | size_t n, page, pages, nfile_ids; |
202 | unsigned long *file_ids = NULL; |
203 | short ret; |
204 | char c; |
205 | bool done, show_list, show_help, find_files; |
206 | |
207 | page = 0; |
208 | show_list = true; |
209 | find_files = true; |
210 | show_help = false; |
211 | done = false; |
212 | |
213 | snprintf(prompt, sizeof(prompt), "Files:%s", folder->name); |
214 | |
215 | while (!done && !s->ending) { |
216 | if (find_files) { |
217 | folder_find_file_ids(folder, &nfile_ids, &file_ids); |
218 | /* ceil(nfile_ids / FILES_PER_PAGE) */ |
219 | pages = (nfile_ids + FILES_PER_PAGE - 1) / FILES_PER_PAGE; |
220 | |
221 | if (page >= pages) |
222 | page = pages - 1; |
223 | |
224 | find_files = false; |
225 | } |
226 | |
227 | if (show_list) { |
228 | folder_list_files(s, folder, nfile_ids, file_ids, page + 1, |
229 | pages); |
230 | show_list = false; |
231 | } |
232 | |
233 | c = session_menu(s, folder->description, prompt, opts, |
234 | nitems(opts), show_help); |
235 | show_help = false; |
236 | |
237 | handle_opt: |
238 | switch (c) { |
239 | case 'l': |
240 | show_list = true; |
241 | break; |
242 | case 'u': |
243 | if (folder_upload(s, folder, NULL, NULL)) |
244 | find_files = true; |
245 | break; |
246 | case '>': |
247 | case '<': |
248 | if (c == '>' && page == pages - 1) { |
249 | session_printf(s, "You are at the last page of files\r\n"); |
250 | session_flush(s); |
251 | break; |
252 | } |
253 | if (c == '<' && page == 0) { |
254 | session_printf(s, "You are already at the first page\r\n"); |
255 | session_flush(s); |
256 | break; |
257 | } |
258 | if (c == '>') |
259 | page++; |
260 | else |
261 | page--; |
262 | show_list = true; |
263 | break; |
264 | case 0: |
265 | case 1: |
266 | case 2: |
267 | case 3: |
268 | case 4: |
269 | case 5: |
270 | case 6: |
271 | case 7: |
272 | case 8: |
273 | case 9: |
274 | if (c >= nfile_ids) { |
275 | session_printf(s, "Invalid file\r\n"); |
276 | session_flush(s); |
277 | break; |
278 | } |
279 | c = folder_file_view(s, folder, file_ids[c], c); |
280 | if (c == FILE_VIEW_RETURN_FIND) |
281 | find_files = true; |
282 | break; |
283 | case '?': |
284 | show_help = true; |
285 | break; |
286 | default: |
287 | done = true; |
288 | break; |
289 | } |
290 | } |
291 | |
292 | if (file_ids != NULL) |
293 | xfree(&file_ids); |
294 | } |
295 | |
296 | void |
297 | folder_list_files(struct session *s, struct folder *folder, |
298 | size_t nfile_ids, unsigned long *file_ids, unsigned long page, |
299 | unsigned long pages) |
300 | { |
301 | char time[24]; |
302 | size_t n, off, size, idx; |
303 | struct bile_object *obj; |
304 | struct folder_file file; |
305 | short j, k; |
306 | char *data; |
307 | |
308 | session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n", |
309 | folder->name, folder->description, page, pages); |
310 | session_printf(s, |
311 | "%s# Date File Description%s\r\n", |
312 | ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END)); |
313 | session_flush(s); |
314 | |
315 | if (nfile_ids == 0) { |
316 | session_printf(s, "No files here yet.\r\n"); |
317 | session_flush(s); |
318 | return; |
319 | } |
320 | |
321 | off = FILES_PER_PAGE * (page - 1); |
322 | |
323 | for (n = 0; n < FILES_PER_PAGE; n++) { |
324 | if (off + n >= nfile_ids) |
325 | break; |
326 | |
327 | size = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, |
328 | file_ids[off + n], &data); |
329 | bile_unmarshall_object(folder->bile, folder_file_object_fields, |
330 | nfolder_file_object_fields, data, size, &file, sizeof(file), |
331 | false, "folder_list_files"); |
332 | xfree(&data); |
333 | |
334 | strftime(time, sizeof(time), "%Y-%m-%d", localtime(&file.time)); |
335 | |
336 | session_printf(s, "%ld %s {{#}}%- 17.17s %- 40s\r\n", |
337 | n, |
338 | time, |
339 | file.filename, |
340 | file.description); |
341 | } |
342 | session_flush(s); |
343 | } |
344 | |
345 | unsigned long |
346 | folder_upload(struct session *s, struct folder *folder, |
347 | char *initial_filename, char *initial_description) |
348 | { |
349 | struct folder_file file = { 0 }; |
350 | FILE *fp; |
351 | SHA1_CTX sha1; |
352 | struct stat sb; |
353 | struct zmodem_session *zs; |
354 | char *upload_path = NULL, *data = NULL, *tmp = NULL, *errorstr = NULL; |
355 | size_t size; |
356 | short c, n, ret, error; |
357 | |
358 | if (!s->user) { |
359 | session_printf(s, "Uploading is not available to guests.\r\n" |
360 | "Please create an account first.\r\n"); |
361 | session_flush(s); |
362 | return 0; |
363 | } |
364 | |
365 | if (initial_filename) |
366 | strlcpy(file.filename, initial_filename, sizeof(file.filename)); |
367 | |
368 | file.uploader_user_id = s->user->id; |
369 | |
370 | session_printf(s, "{{B}}Upload New File{{/B}}\r\n"); |
371 | session_printf(s, |
372 | "To begin your ZMODEM upload, press Enter and choose a file through\r\n"); |
373 | session_printf(s, |
374 | "your terminal program (only one file can be uploaded at a time).\r\n"); |
375 | session_printf(s, |
376 | "\r\n" |
377 | "You'll then be given the option to change the filename and enter a\r\n" |
378 | "description for the file.\r\n" |
379 | "\r\n" |
380 | "To cancel your upload, press ^C a few times.\r\n\r\n"); |
381 | session_flush(s); |
382 | session_pause_return(s, 0, "when you are ready..."); |
383 | |
384 | upload_path = xmalloc(FILENAME_MAX, "folder_upload path"); |
385 | snprintf(upload_path, FILENAME_MAX, "%s:upload-%08lx%08lx", |
386 | folder->path, xorshift32(), xorshift32()); |
387 | |
388 | zs = ZCreateReceiver(s, upload_path); |
389 | zs->DoIACEscape = s->is_telnet; |
390 | |
391 | ZInit(zs); |
392 | s->transferring_file = true; |
393 | session_flush(s); |
394 | session_logf(s, "[%s] Receiving uploaded file to %s", folder->name, |
395 | upload_path); |
396 | while (!s->ending) { |
397 | if (ZHaveTimedOut(zs)) { |
398 | ZTimeOutProc(zs); |
399 | session_logf(s, "[%s] Transfer timed out, canceling", |
400 | folder->name); |
401 | break; |
402 | } |
403 | |
404 | if (!ZParse(zs)) |
405 | break; |
406 | |
407 | if (s->obuflen) |
408 | session_flush(s); |
409 | } |
410 | |
411 | /* flush the pipes */ |
412 | s->obuflen = 0; |
413 | uthread_msleep(1000); |
414 | session_clear_input(s); |
415 | s->transferring_file = false; |
416 | |
417 | if (zs->file) { |
418 | fclose(zs->file); |
419 | zs->file = NULL; |
420 | } |
421 | |
422 | session_printf(s, "\r\n\r\n"); |
423 | |
424 | if (stat(upload_path, &sb) != 0) { |
425 | session_logf(s, "[%s] Failed receiving upload, no temp file", |
426 | folder->name); |
427 | session_printf(s, "Failed receiving file, aborting.\r\n"); |
428 | session_pause_return(s, CONTROL_C, "to continue..."); |
429 | ZDestroy(zs); |
430 | zs = NULL; |
431 | xfree(&upload_path); |
432 | return 0; |
433 | } |
434 | |
435 | if (sb.st_size != zs->file_size) { |
436 | session_logf(s, "[%s] Received uploaded file of size %ld but " |
437 | "supposed to be %ld, canceling", folder->name, sb.st_size, |
438 | zs->file_size); |
439 | session_printf(s, |
440 | "Uploaded file expected to be {{B}}%ld{{/B}} byte%s, but " |
441 | "received\r\n" |
442 | "{{B}}%ld{{/B}} byte%s. Deleting file and canceling upload.\r\n", |
443 | zs->file_size, zs->file_size == 1 ? "" : "s", |
444 | sb.st_size, sb.st_size == 1 ? "" : "s"); |
445 | session_pause_return(s, CONTROL_C, "to continue..."); |
446 | ZDestroy(zs); |
447 | zs = NULL; |
448 | goto file_upload_cancel; |
449 | } |
450 | |
451 | strlcpy(file.filename, zs->file_name, sizeof(file.filename)); |
452 | file.size = sb.st_size; |
453 | |
454 | session_printf(s, "Successfully received file {{B}}%s{{/B}} of size " |
455 | "{{B}}%ld{{/B}} byte%s.\r\n", |
456 | file.filename, file.size, file.size == 1 ? "" : "s"); |
457 | session_pause_return(s, '\r', "to continue..."); |
458 | |
459 | session_logf(s, "[%s] Received uploaded file %s of size %ld", |
460 | folder->name, file.filename, file.size); |
461 | |
462 | /* this will fclose and free zs */ |
463 | ZDestroy(zs); |
464 | zs = NULL; |
465 | |
466 | session_printf(s, "Calculating SHA1 checksum of uploaded file..."); |
467 | session_flush(s); |
468 | data = xmalloc(1024, "folder_upload data"); |
469 | SHA1Init(&sha1); |
470 | fp = fopen(upload_path, "rb"); |
471 | for (n = 1; fp && !feof(fp); n++) { |
472 | size = fread(data, 1024, 1, fp); |
473 | SHA1Update(&sha1, (const u_int8_t *)data, size); |
474 | if (n % 2 == 0) |
475 | uthread_yield(); |
476 | } |
477 | SHA1End(&sha1, (char *)&file.sha1_checksum); |
478 | if (fp) |
479 | fclose(fp); |
480 | xfree(&data); |
481 | |
482 | session_printf(s, "done.\r\n\r\n"); |
483 | session_flush(s); |
484 | session_printf(s, "{{B}}Uploaded By:{{/B}} %s\r\n", |
485 | s->user->username); |
486 | session_printf(s, "{{B}}Folder:{{/B}} %s\r\n", |
487 | folder->name); |
488 | session_printf(s, "{{B}}SHA1 Checksum:{{/B}} %s\r\n", |
489 | file.sha1_checksum); |
490 | session_printf(s, "{{B}}File Size:{{/B}} %ld\r\n", file.size); |
491 | session_flush(s); |
492 | |
493 | file_upload_annotate: |
494 | for (;;) { |
495 | session_printf(s, "{{B}}File Name:{{/B}} "); |
496 | session_flush(s); |
497 | |
498 | tmp = session_field_input(s, FOLDER_FILE_FILENAME_LENGTH, |
499 | FOLDER_FILE_FILENAME_LENGTH - 1, file.filename, false, 0); |
500 | if (tmp == NULL) |
501 | goto file_upload_cancel; |
502 | strlcpy(file.filename, tmp, sizeof(file.filename)); |
503 | xfree(&tmp); |
504 | session_output(s, "\r\n", 2); |
505 | session_flush(s); |
506 | |
507 | rtrim(file.filename, "\r\n\t "); |
508 | |
509 | if (!folder_file_valid_filename(s, folder, &file, &errorstr)) { |
510 | session_printf(s, "{{B}}Error:{{/B}} %s\r\n", errorstr); |
511 | xfree(&errorstr); |
512 | continue; |
513 | } |
514 | |
515 | break; |
516 | } |
517 | |
518 | for (;;) { |
519 | session_printf(s, "{{B}}Short Description:{{/B}} "); |
520 | session_flush(s); |
521 | |
522 | tmp = session_field_input(s, FOLDER_FILE_DESCR_LENGTH, |
523 | FOLDER_FILE_DESCR_LENGTH - 1, file.description, false, 0); |
524 | if (tmp == NULL) |
525 | goto file_upload_cancel; |
526 | strlcpy(file.description, tmp, sizeof(file.description)); |
527 | xfree(&tmp); |
528 | session_output(s, "\r\n", 2); |
529 | session_flush(s); |
530 | |
531 | rtrim(file.description, "\r\n\t "); |
532 | |
533 | if (file.description[0] == '\0') { |
534 | session_printf(s, "{{B}}Error:{{/B}} File " |
535 | "description cannot be blank (^C to cancel)\r\n"); |
536 | session_flush(s); |
537 | continue; |
538 | } |
539 | |
540 | break; |
541 | } |
542 | |
543 | for (;;) { |
544 | session_printf(s, |
545 | "{{B}}Notes (Optional, ^D when finished):{{/B}}\r\n"); |
546 | session_flush(s); |
547 | |
548 | tmp = session_field_input(s, 2048, s->terminal_columns - 1, |
549 | file.notes, true, 0); |
550 | if (file.notes != NULL) |
551 | xfree(&file.notes); |
552 | file.notes = tmp; |
553 | session_output(s, "\r\n", 2); |
554 | session_flush(s); |
555 | if (file.notes == NULL) { |
556 | file.notes_size = 0; |
557 | break; |
558 | } |
559 | |
560 | rtrim(file.notes, "\r\n\t "); |
561 | |
562 | if (file.notes[0] == '\0') { |
563 | xfree(&file.notes); |
564 | file.notes = NULL; |
565 | file.notes_size = 0; |
566 | break; |
567 | } |
568 | file.notes_size = strlen(file.notes) + 1; |
569 | break; |
570 | } |
571 | |
572 | for (;;) { |
573 | session_printf(s, "\r\n{{B}}(S){{/B}}ave file, " |
574 | "{{B}}(E){{/B}}dit again, or {{B}}(C){{/B}}ancel? "); |
575 | session_flush(s); |
576 | |
577 | c = session_input_char(s); |
578 | if (c == 0 || s->ending) |
579 | goto file_upload_done; |
580 | |
581 | switch (c) { |
582 | case 's': |
583 | case 'S': |
584 | case 'y': |
585 | session_printf(s, "%c\r\n", c); |
586 | session_flush(s); |
587 | /* FALLTHROUGH */ |
588 | case '\n': |
589 | case '\r': |
590 | /* save */ |
591 | session_printf(s, "Saving file... "); |
592 | session_flush(s); |
593 | |
594 | if (folder_file_save(folder, &file, upload_path)) { |
595 | session_logf(s, "[%s] Saved file %s", folder->name, |
596 | file.filename); |
597 | session_printf(s, "done\r\n"); |
598 | session_flush(s); |
599 | goto file_upload_done; |
600 | } else { |
601 | session_printf(s, "failed!\r\n"); |
602 | session_flush(s); |
603 | goto file_upload_cancel; |
604 | } |
605 | break; |
606 | case 'e': |
607 | case 'E': |
608 | session_printf(s, "%c\r\n", c); |
609 | session_flush(s); |
610 | goto file_upload_annotate; |
611 | case 'c': |
612 | case 'C': |
613 | session_printf(s, "%c\r\n", c); |
614 | session_flush(s); |
615 | /* FALLTHROUGH */ |
616 | case CONTROL_C: |
617 | goto file_upload_cancel; |
618 | } |
619 | } |
620 | |
621 | file_upload_error: |
622 | session_printf(s, "Failed saving file!\r\n"); |
623 | session_flush(s); |
624 | |
625 | file_upload_cancel: |
626 | session_printf(s, "\r\n"); |
627 | session_flush(s); |
628 | |
629 | if (upload_path[0] != '\0') { |
630 | CtoPstr(upload_path); |
631 | error = FSDelete(upload_path, 0); |
632 | PtoCstr(upload_path); |
633 | if (error) { |
634 | session_logf(s, "[%s] Failed deleting temporary uploaded " |
635 | "file %s: %d", folder->name, upload_path, error); |
636 | } else { |
637 | session_logf(s, "[%s] Canceled upload, deleted temp file %s", |
638 | folder->name, upload_path); |
639 | } |
640 | } |
641 | |
642 | file_upload_done: |
643 | if (file.notes) |
644 | xfree(&file.notes); |
645 | if (upload_path != NULL) |
646 | xfree(&upload_path); |
647 | |
648 | return file.id; |
649 | } |
650 | |
651 | short |
652 | folder_file_view(struct session *s, struct folder *folder, |
653 | unsigned long id, short idx) |
654 | { |
655 | static const struct session_menu_option opts[] = { |
656 | { '#', "#0123456789", "View file [#]" }, |
657 | { 'd', "Dd", "Download this file" }, |
658 | { 'r', "Rr", "Remove this file" }, |
659 | { 'e', "Ee", "Edit this file" }, |
660 | { 'q', "QqXx", "Return to folder" }, |
661 | { '?', "?", "List these options" }, |
662 | }; |
663 | char time[32]; |
664 | struct folder_file file; |
665 | struct username_cache *uploader; |
666 | struct zmodem_session *zs; |
667 | struct session_menu_option *dopts = NULL; |
668 | FILE *fp; |
669 | char *path = NULL; |
670 | char prompt[6 + BOARD_NAME_LENGTH + 1 + FOLDER_FILE_FILENAME_LENGTH]; |
671 | size_t n, size; |
672 | char c; |
673 | char *data; |
674 | short cc, ret; |
675 | bool done = false, show_help = false; |
676 | |
677 | size = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, id, &data); |
678 | if (size == 0) |
679 | panic("failed fetching message %ld: %d", id, |
680 | bile_error(folder->bile)); |
681 | bile_unmarshall_object(folder->bile, folder_file_object_fields, |
682 | nfolder_file_object_fields, data, size, &file, sizeof(file), true, |
683 | "folder_file_view"); |
684 | xfree(&data); |
685 | |
686 | dopts = xmalloc(sizeof(opts), "folder_file_view opts"); |
687 | memcpy(dopts, opts, sizeof(opts)); |
688 | if (!(s->user && (s->user->is_sysop || |
689 | s->user->id == file.uploader_user_id))) { |
690 | /* disable deleting and editing */ |
691 | dopts[2].key[0] = '\0'; |
692 | dopts[3].key[0] = '\0'; |
693 | } |
694 | |
695 | uploader = user_username(file.uploader_user_id); |
696 | |
697 | strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S", |
698 | localtime(&file.time)); |
699 | |
700 | session_printf(s, "{{B}}Folder:{{/B}} %s\r\n", folder->name); |
701 | session_printf(s, "{{B}}File Name:{{/B}} %s\r\n", file.filename); |
702 | session_printf(s, "{{B}}Description:{{/B}} %s\r\n", file.description); |
703 | session_printf(s, "{{B}}File Size:{{/B}} %lu\r\n", file.size); |
704 | session_printf(s, "{{B}}File SHA1:{{/B}} %s\r\n", file.sha1_checksum); |
705 | session_printf(s, "{{B}}Uploaded:{{/B}} %s %s\r\n", time, |
706 | db->config.timezone); |
707 | session_printf(s, "{{B}}Uploaded By:{{/B}} %s\r\n", |
708 | uploader ? uploader->username : "(unknown)"); |
709 | session_flush(s); |
710 | |
711 | if (file.notes_size > 0) { |
712 | session_printf(s, "{{B}}Notes:{{/B}}\r\n"); |
713 | session_output(s, file.notes, file.notes_size); |
714 | session_printf(s, "\r\n"); |
715 | } |
716 | |
717 | session_printf(s, |
718 | "\r\n[ Not viewing file contents, press 'd' to download. ]\r\n"); |
719 | session_flush(s); |
720 | |
721 | snprintf(prompt, sizeof(prompt), "Files:%s:%s", folder->name, |
722 | file.filename); |
723 | |
724 | while (!done && !s->ending) { |
725 | c = session_menu(s, file.filename, prompt, dopts, nitems(opts), |
726 | show_help); |
727 | show_help = false; |
728 | |
729 | switch (c) { |
730 | case 'd': |
731 | path = xmalloc(FILENAME_MAX, "folder_file_view filename"); |
732 | snprintf(path, FILENAME_MAX, "%s:%s", folder->path, |
733 | file.filename); |
734 | fp = fopen(path, "rb"); |
735 | if (!fp) { |
736 | session_logf(s, "[%s] Failed opening file %s", |
737 | folder->name, path); |
738 | session_printf(s, |
739 | "{{B}}Error:{{/B}} Failed opening file\r\n"); |
740 | xfree(&path); |
741 | break; |
742 | } |
743 | |
744 | zs = ZCreateSender(s, fp, file.filename); |
745 | zs->DoIACEscape = s->is_telnet; |
746 | |
747 | ZInit(zs); |
748 | s->transferring_file = true; |
749 | session_logf(s, "[%s] Downloading file %s", folder->name, |
750 | file.filename); |
751 | for (;;) { |
752 | if (ZHaveTimedOut(zs)) { |
753 | ZTimeOutProc(zs); |
754 | session_logf(s, "[%s] Transfer timed out, canceling", |
755 | folder->name); |
756 | break; |
757 | } |
758 | |
759 | if (!ZParse(zs)) |
760 | break; |
761 | |
762 | if (s->obuflen) |
763 | session_flush(s); |
764 | } |
765 | |
766 | /* flush the pipes */ |
767 | s->obuflen = 0; |
768 | uthread_msleep(1000); |
769 | session_clear_input(s); |
770 | s->transferring_file = false; |
771 | |
772 | if (zs->file) { |
773 | fclose(zs->file); |
774 | zs->file = NULL; |
775 | } |
776 | ZDestroy(zs); |
777 | zs = NULL; |
778 | xfree(&path); |
779 | |
780 | session_printf(s, "\r\n"); |
781 | session_flush(s); |
782 | session_pause_return(s, CONTROL_C, "to continue..."); |
783 | break; |
784 | case 'e': |
785 | if (!(s->user && (s->user->is_sysop || |
786 | s->user->id == file.uploader_user_id))) { |
787 | session_printf(s, "Invalid option\r\n"); |
788 | session_flush(s); |
789 | break; |
790 | } |
791 | folder_edit_file(s, folder, &file); |
792 | ret = FILE_VIEW_RETURN_FIND; |
793 | done = true; |
794 | break; |
795 | case 'r': |
796 | if (!(s->user && (s->user->is_sysop || |
797 | s->user->id == file.uploader_user_id))) { |
798 | session_printf(s, "Invalid option\r\n"); |
799 | session_flush(s); |
800 | break; |
801 | } |
802 | |
803 | session_printf(s, "Are you sure you want to permanently " |
804 | "delete this file? [y/N] "); |
805 | session_flush(s); |
806 | |
807 | cc = session_input_char(s); |
808 | if (cc == 'y' || c == 'Y') { |
809 | session_printf(s, "%c\r\n", cc); |
810 | session_flush(s); |
811 | |
812 | folder_delete_file(s, folder, &file); |
813 | |
814 | session_printf(s, "\r\n{{B}}File deleted!{{/B}}\r\n"); |
815 | session_flush(s); |
816 | ret = FILE_VIEW_RETURN_FIND; |
817 | done = true; |
818 | } else { |
819 | session_printf(s, "\r\Post not deleted.\r\n"); |
820 | session_flush(s); |
821 | } |
822 | break; |
823 | case 0: |
824 | case 1: |
825 | case 2: |
826 | case 3: |
827 | case 4: |
828 | case 5: |
829 | case 6: |
830 | case 7: |
831 | case 8: |
832 | case 9: |
833 | ret = c; |
834 | done = true; |
835 | break; |
836 | case 'q': |
837 | done = true; |
838 | break; |
839 | case '?': |
840 | show_help = true; |
841 | break; |
842 | } |
843 | } |
844 | |
845 | if (file.notes != NULL) |
846 | xfree(&file.notes); |
847 | |
848 | xfree(&dopts); |
849 | |
850 | return ret; |
851 | } |
852 | |
853 | size_t |
854 | folder_find_file_ids(struct folder *folder, size_t *nfile_ids, |
855 | unsigned long **file_ids) |
856 | { |
857 | struct bile_object *o; |
858 | struct folder_file file; |
859 | struct file_name_map { |
860 | unsigned long id; |
861 | char filename[10]; /* only sorted to 10 character places */ |
862 | } *name_map = NULL, tmp_map; |
863 | size_t n, size; |
864 | short i, j; |
865 | char *data; |
866 | |
867 | *nfile_ids = bile_count_by_type(folder->bile, FOLDER_FILE_RTYPE); |
868 | if (*nfile_ids == 0) |
869 | return 0; |
870 | |
871 | name_map = xcalloc(*nfile_ids, sizeof(struct file_name_map), |
872 | "folder_find_file_ids"); |
873 | |
874 | for (n = 0; o = bile_get_nth_of_type(folder->bile, n, |
875 | FOLDER_FILE_RTYPE); n++) { |
876 | if (n >= *nfile_ids) |
877 | break; |
878 | bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, o->id, &data); |
879 | bile_unmarshall_object(folder->bile, folder_file_object_fields, |
880 | nfolder_file_object_fields, data, o->size, &file, sizeof(file), |
881 | false, "folder_find_file_ids"); |
882 | xfree(&data); |
883 | xfree(&o); |
884 | |
885 | name_map[n].id = file.id; |
886 | strlcpy(name_map[n].filename, file.filename, |
887 | sizeof(name_map[n].filename)); |
888 | } |
889 | |
890 | /* sort by filename */ |
891 | for (i = 0; i < *nfile_ids; i++) { |
892 | for (j = 0; j < *nfile_ids - i - 1; j++) { |
893 | if (strcasecmp(name_map[j].filename, |
894 | name_map[j + 1].filename) > 0) { |
895 | tmp_map = name_map[j]; |
896 | name_map[j] = name_map[j + 1]; |
897 | name_map[j + 1] = tmp_map; |
898 | } |
899 | } |
900 | } |
901 | |
902 | *file_ids = xcalloc(sizeof(long), *nfile_ids, "folder_find_file_ids"); |
903 | for (i = 0; i < *nfile_ids; i++) |
904 | (*file_ids)[i] = name_map[i].id; |
905 | |
906 | xfree(&name_map); |
907 | |
908 | done: |
909 | return *nfile_ids; |
910 | } |
911 | |
912 | bool |
913 | folder_file_save(struct folder *folder, struct folder_file *file, |
914 | char *temp_path) |
915 | { |
916 | char note[MALLOC_NOTE_SIZE]; |
917 | char *new_name = NULL; |
918 | short ret; |
919 | char *data; |
920 | size_t size; |
921 | ssize_t n, j; |
922 | size_t insert; |
923 | |
924 | file->id = bile_next_id(folder->bile, FOLDER_FILE_RTYPE); |
925 | file->time = Time; |
926 | |
927 | snprintf(note, sizeof(note), "folder_file_save %ld", file->id); |
928 | ret = bile_marshall_object(folder->bile, folder_file_object_fields, |
929 | nfolder_file_object_fields, file, &data, &size, note); |
930 | if (ret != 0 || size == 0) { |
931 | warn("failed to marshall new file object"); |
932 | file->id = 0; |
933 | return false; |
934 | } |
935 | if (bile_write(folder->bile, FOLDER_FILE_RTYPE, file->id, data, |
936 | size) != size) { |
937 | warn("bile_write of new file failed! %d", bile_error(folder->bile)); |
938 | file->id = 0; |
939 | xfree(&data); |
940 | return false; |
941 | } |
942 | xfree(&data); |
943 | |
944 | bile_flush(folder->bile, true); |
945 | |
946 | new_name = xmalloc(FILENAME_MAX, "folder_file_save"); |
947 | snprintf(new_name, FILENAME_MAX, "%s:%s", folder->path, |
948 | file->filename); |
949 | CtoPstr(temp_path); |
950 | CtoPstr(new_name); |
951 | ret = Rename(temp_path, 0, new_name); |
952 | PtoCstr(temp_path); |
953 | PtoCstr(new_name); |
954 | if (ret != 0) { |
955 | warn("FSRename(%s, 0, %s) failed: %d", temp_path, new_name, ret); |
956 | bile_delete(folder->bile, FOLDER_FILE_RTYPE, file->id); |
957 | file->id = 0; |
958 | xfree(&new_name); |
959 | return false; |
960 | } |
961 | |
962 | xfree(&new_name); |
963 | return true; |
964 | } |
965 | |
966 | void |
967 | folder_delete_file(struct session *s, struct folder *folder, |
968 | struct folder_file *file) |
969 | { |
970 | char *path = NULL; |
971 | short ret; |
972 | |
973 | bile_delete(folder->bile, FOLDER_FILE_RTYPE, file->id); |
974 | bile_flush(folder->bile, true); |
975 | |
976 | path = xmalloc(FILENAME_MAX, "folder_delete_file"); |
977 | snprintf(path, FILENAME_MAX, "%s:%s", folder->path, file->filename); |
978 | CtoPstr(path); |
979 | ret = FSDelete(path, 0); |
980 | PtoCstr(path); |
981 | |
982 | if (ret != 0) |
983 | session_logf(s, "[%s] Failed deleting %s: %d", folder->name, |
984 | path, ret); |
985 | |
986 | xfree(&path); |
987 | if (file->notes != NULL) |
988 | xfree(&file->notes); |
989 | |
990 | session_logf(s, "[%s] Deleted file %s (%ld)", folder->name, |
991 | file->filename, file->id); |
992 | } |
993 | |
994 | bool |
995 | folder_file_valid_filename(struct session *session, |
996 | struct folder *folder, struct folder_file *file, char **error) |
997 | { |
998 | struct bile_object *o; |
999 | struct folder_file tfile; |
1000 | size_t len, n; |
1001 | char *data; |
1002 | char c; |
1003 | |
1004 | if (file->filename[0] == '\0') { |
1005 | *error = xstrdup("filename cannot be empty", |
1006 | "folder_file_valid_filename"); |
1007 | return false; |
1008 | } |
1009 | |
1010 | len = strlen(file->filename); |
1011 | if (len > FOLDER_FILE_FILENAME_LENGTH) { |
1012 | *error = xmalloc(61, "folder_file_valid_filename"); |
1013 | snprintf(*error, 60, "filename cannot be more than %d characters", |
1014 | FOLDER_FILE_FILENAME_LENGTH); |
1015 | return false; |
1016 | } |
1017 | |
1018 | for (n = 0; n < len; n++) { |
1019 | c = file->filename[n]; |
1020 | |
1021 | if (n == 0 && c == '.') { |
1022 | *error = xstrdup("filename cannot start with a dot", |
1023 | "folder_file_valid_filename"); |
1024 | return false; |
1025 | } |
1026 | |
1027 | if (n == len - 1 && c == ' ') { |
1028 | *error = xstrdup("filename cannot end with a space", |
1029 | "folder_file_valid_filename"); |
1030 | return false; |
1031 | } |
1032 | |
1033 | if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || |
1034 | (c >= 'a' && c <= 'z') || c == '_' || c == '-' || c == '+' || |
1035 | c == '*' || c == ' ' || c == '.' || c == ',')) { |
1036 | *error = xmalloc(61, "folder_file_valid_filename"); |
1037 | snprintf(*error, 60, "filename cannot contain '%c' character", |
1038 | c); |
1039 | return false; |
1040 | } |
1041 | } |
1042 | |
1043 | for (n = 0; o = bile_get_nth_of_type(folder->bile, n, |
1044 | FOLDER_FILE_RTYPE); n++) { |
1045 | if (o->id == file->id) { |
1046 | xfree(&o); |
1047 | continue; |
1048 | } |
1049 | |
1050 | bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, o->id, &data); |
1051 | bile_unmarshall_object(folder->bile, folder_file_object_fields, |
1052 | nfolder_file_object_fields, data, o->size, &tfile, sizeof(tfile), |
1053 | false, "folder_file_valid_filename"); |
1054 | xfree(&data); |
1055 | xfree(&o); |
1056 | |
1057 | if (strcasecmp(file->filename, tfile.filename) == 0) { |
1058 | *error = xstrdup("filename is already taken", |
1059 | "folder_file_valid_filename"); |
1060 | return false; |
1061 | } |
1062 | } |
1063 | |
1064 | return true; |
1065 | } |
1066 | |
1067 | void |
1068 | folder_edit_file(struct session *s, struct folder *folder, |
1069 | struct folder_file *file) |
1070 | { |
1071 | /* TODO */ |
1072 | } |