AmendHub

Download

jcs

/

subtext

/

folder.c

 

(View History)

jcs   *: Minor cleanups, remove dead variables, fix some error paths Latest amendment: 580 on 2024-01-24

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 "nomodem.h"
26 #include "sha1.h"
27 #include "user.h"
28 #include "zmodem.h"
29
30 #define FILES_PER_PAGE 20
31
32 #define FILE_VIEW_RETURN_DONE -1
33 #define FILE_VIEW_RETURN_LIST -2
34 #define FILE_VIEW_RETURN_FIND -3
35
36 const struct struct_field folder_fields[] = {
37 { "Folder ID", CONFIG_TYPE_LONG,
38 offsetof(struct folder, id),
39 1, LONG_MAX },
40 { "Name", CONFIG_TYPE_STRING,
41 offsetof(struct folder, name),
42 1, member_size(struct folder, name) },
43 { "Disk Path", CONFIG_TYPE_STRING,
44 offsetof(struct folder, path),
45 1, member_size(struct folder, path) },
46 { "Description", CONFIG_TYPE_STRING,
47 offsetof(struct folder, description),
48 0, member_size(struct folder, description) },
49 { "Restricted Posting", CONFIG_TYPE_BOOLEAN,
50 offsetof(struct folder, restricted_posting),
51 0, 0 },
52 { "Restricted Viewing", CONFIG_TYPE_BOOLEAN,
53 offsetof(struct folder, restricted_viewing),
54 0, 0 },
55 };
56 const size_t nfolder_fields = nitems(folder_fields);
57
58 const struct bile_object_field folder_object_fields[] = {
59 { offsetof(struct folder, id),
60 member_size(struct folder, id), -1 },
61 { offsetof(struct folder, name),
62 member_size(struct folder, name), -1 },
63 { offsetof(struct folder, description),
64 member_size(struct folder, description), -1 },
65 { offsetof(struct folder, restricted_posting),
66 member_size(struct folder, restricted_posting), -1 },
67 { offsetof(struct folder, restricted_viewing),
68 member_size(struct folder, restricted_viewing), -1 },
69 { offsetof(struct folder, last_upload_at),
70 member_size(struct folder, last_upload_at), -1 },
71 { offsetof(struct folder, file_count),
72 member_size(struct folder, file_count), -1 },
73 { offsetof(struct folder, path),
74 member_size(struct folder, path), -1 },
75 };
76 const size_t nfolder_object_fields = nitems(folder_object_fields);
77
78 const struct bile_object_field folder_file_object_fields[] = {
79 { offsetof(struct folder_file, id),
80 member_size(struct folder_file, id), -1 },
81 { offsetof(struct folder_file, time),
82 member_size(struct folder_file, time), -1 },
83 { offsetof(struct folder_file, uploader_user_id),
84 member_size(struct folder_file, uploader_user_id), -1 },
85 { offsetof(struct folder_file, filename),
86 member_size(struct folder_file, filename), -1 },
87 { offsetof(struct folder_file, description),
88 member_size(struct folder_file, description), -1 },
89 { offsetof(struct folder_file, size),
90 member_size(struct folder_file, size), -1 },
91 { offsetof(struct folder_file, sha1_checksum),
92 member_size(struct folder_file, sha1_checksum), -1 },
93 { offsetof(struct folder_file, notes_size),
94 member_size(struct folder_file, notes_size), -1 },
95 { offsetof(struct folder_file, notes),
96 -1, offsetof(struct folder_file, notes_size) },
97 };
98 const size_t nfolder_file_object_fields = nitems(folder_file_object_fields);
99
100 unsigned long folder_upload(struct session *s, struct folder *folder,
101 char *initial_filename, char *initial_description);
102 void folder_list_files(struct session *s, struct folder *folder,
103 size_t nfile_ids, unsigned long *file_ids, short files_per_page,
104 size_t page, size_t pages);
105 void folder_show(struct session *s, struct folder *folder);
106 short folder_file_view(struct session *s, struct folder *folder,
107 unsigned long id);
108 void folder_delete_file(struct session *s, struct folder *folder,
109 struct folder_file *file);
110 size_t folder_find_file_ids(struct folder *folder, size_t *nfile_ids,
111 unsigned long **file_ids);
112 bool folder_file_save(struct folder *folder, struct folder_file *file,
113 char *temp_path);
114 bool folder_file_valid_filename(struct session *session,
115 struct folder *folder, struct folder_file *file, char **error);
116 bool folder_edit_file(struct session *s, struct folder *folder,
117 struct folder_file *file, char *file_path);
118 bool folder_file_checksum(struct session *s, struct folder_file *file,
119 char *file_path);
120
121 void
122 folder_list(struct session *s)
123 {
124 static const struct session_menu_option opts[] = {
125 { '#', "#", "View file folder [#]" },
126 { 'l', "Ll", "List folders" },
127 { 'q', "QqXx", "Return to main menu" },
128 { '?', "?", "List menu options" },
129 };
130 static const char prompt_help[] =
131 "#:View Folder L:List Q:Return ?:Help";
132 short n, fn;
133 char c;
134 bool done, show_list, show_help;
135
136 show_list = true;
137 show_help = false;
138 done = false;
139
140 while (!done && !s->ending) {
141 if (show_list) {
142 session_printf(s, "{{B}}File Folders{{/B}}\r\n");
143 session_printf(s, "%s# Name Description%s\r\n",
144 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
145 session_flush(s);
146
147 for (n = 0; n < db->nfolders; n++) {
148 session_printf(s, "%2d %- 13.13s %s\r\n",
149 n + 1,
150 db->folders[n].name,
151 db->folders[n].description);
152 }
153 session_flush(s);
154
155 show_list = false;
156 }
157
158 c = session_menu(s, "File Folders", "Files", (char *)prompt_help,
159 opts, nitems(opts), show_help, "Folder #", &fn);
160 show_help = false;
161
162 handle_opt:
163 switch (c) {
164 case 'l':
165 show_list = true;
166 break;
167 case '#':
168 check_fn:
169 if (fn < 1 || fn > db->nfolders) {
170 session_printf(s, "Invalid folder\r\n");
171 session_flush(s);
172 break;
173 }
174 folder_show(s, &db->folders[fn - 1]);
175 break;
176 case '?':
177 show_help = true;
178 break;
179 default:
180 done = true;
181 break;
182 }
183 }
184 }
185
186 void
187 folder_show(struct session *s, struct folder *folder)
188 {
189 static const struct session_menu_option opts[] = {
190 { '#', "#", "View file [#]" },
191 { '<', "<", "Newer page of files" },
192 { 'l', "Ll", "List files" },
193 { '>', ">", "Older page of files" },
194 { 'u', "Uu", "Upload new file" },
195 { 'q', "QqXx", "Return to main menu" },
196 { '?', "?", "List menu options" },
197 };
198 static const char prompt_help[] =
199 "#:View <:Newer >:Older L:List U:Upload Q:Return ?:Help";
200 char prompt[6 + member_size(struct folder, name) + 8];
201 size_t page, pages, nfile_ids;
202 unsigned long *file_ids = NULL;
203 short fpp, fn;
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 fpp = FILES_PER_PAGE;
219 if (s->terminal_lines < fpp + 3)
220 fpp = BOUND(fpp, 5, s->terminal_lines - 3);
221 /* ceil(nfile_ids / fpp) */
222 pages = (nfile_ids + fpp - 1) / fpp;
223
224 if (page >= pages)
225 page = pages - 1;
226
227 find_files = false;
228 }
229
230 if (show_list) {
231 folder_list_files(s, folder, nfile_ids, file_ids, fpp,
232 page + 1, pages);
233 show_list = false;
234 }
235
236 c = session_menu(s, folder->description, prompt, (char *)prompt_help,
237 opts, nitems(opts), show_help, "File #", &fn);
238 show_help = false;
239
240 handle_opt:
241 switch (c) {
242 case 'l':
243 show_list = true;
244 break;
245 case 'u':
246 if (folder_upload(s, folder, NULL, NULL))
247 find_files = true;
248 break;
249 case '>':
250 case '<':
251 if (c == '>' && page == pages - 1) {
252 session_printf(s, "You are at the last page of files\r\n");
253 session_flush(s);
254 break;
255 }
256 if (c == '<' && page == 0) {
257 session_printf(s, "You are already at the first page\r\n");
258 session_flush(s);
259 break;
260 }
261 if (c == '>')
262 page++;
263 else
264 page--;
265 show_list = true;
266 break;
267 case '#':
268 if (fn < 1 || fn > nfile_ids) {
269 session_printf(s, "Invalid file\r\n");
270 session_flush(s);
271 break;
272 }
273 c = folder_file_view(s, folder, file_ids[fn - 1]);
274 if (c == FILE_VIEW_RETURN_FIND)
275 find_files = true;
276 break;
277 case '?':
278 show_help = true;
279 break;
280 default:
281 done = true;
282 break;
283 }
284 }
285
286 if (file_ids != NULL)
287 xfree(&file_ids);
288 }
289
290 void
291 folder_list_files(struct session *s, struct folder *folder,
292 size_t nfile_ids, unsigned long *file_ids, short files_per_page,
293 unsigned long page, unsigned long pages)
294 {
295 char time[24];
296 size_t n, off, size;
297 struct folder_file file;
298 short ret;
299 char *data;
300
301 session_printf(s, "{{B}}%s: %s (Page %ld of %ld){{/B}}\r\n",
302 folder->name, folder->description, page, pages);
303 session_printf(s,
304 "%s # Date File Description%s\r\n",
305 ansi(s, ANSI_BOLD, ANSI_END), ansi(s, ANSI_RESET, ANSI_END));
306 session_flush(s);
307
308 if (nfile_ids == 0) {
309 session_printf(s, "No files here yet.\r\n");
310 session_flush(s);
311 return;
312 }
313
314 off = files_per_page * (page - 1);
315
316 for (n = 0; n < files_per_page; n++) {
317 if (off + n >= nfile_ids)
318 break;
319
320 size = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE,
321 file_ids[off + n], &data);
322 ret = bile_unmarshall_object(folder->bile, folder_file_object_fields,
323 nfolder_file_object_fields, data, size, &file, sizeof(file),
324 false);
325 xfree(&data);
326 if (ret == BILE_ERR_NO_MEMORY)
327 return;
328
329 strftime(time, sizeof(time), "%Y-%m-%d", localtime(&file.time));
330
331 session_printf(s, "%2ld %s {{#}}%- 17.17s %- 40s\r\n",
332 n + 1,
333 time,
334 file.filename,
335 file.description);
336 }
337 session_flush(s);
338 }
339
340 unsigned long
341 folder_upload(struct session *s, struct folder *folder,
342 char *initial_filename, char *initial_description)
343 {
344 struct folder_file file = { 0 };
345 struct stat sb;
346 struct zmodem_session *zs;
347 struct nomodem_session *ns;
348 char *upload_path = NULL, *file_name;
349 size_t file_size;
350 short error;
351
352 if (!s->user) {
353 session_printf(s, "Uploading is not available to guests.\r\n"
354 "Please create an account first.\r\n");
355 session_flush(s);
356 return 0;
357 }
358
359 if (initial_filename)
360 strlcpy(file.filename, initial_filename, sizeof(file.filename));
361
362 file.uploader_user_id = s->user->id;
363
364 upload_path = xmalloc(FILENAME_MAX);
365 if (upload_path == NULL)
366 return 0;
367
368 session_printf(s, "{{B}}Upload New File{{/B}}\r\n");
369 if (s->can_nomodem) {
370 session_printf(s,
371 "To begin your upload, press Enter and choose a file (only one file\r\n");
372 session_printf(s,
373 "can be uploaded at a time).\r\n");
374 } else {
375 session_printf(s,
376 "To begin your ZMODEM upload, press Enter and choose a file through\r\n");
377 session_printf(s,
378 "your terminal program (only one file can be uploaded at a time).\r\n");
379 }
380 session_printf(s,
381 "\r\n"
382 "You'll then be given the option to change the filename and enter a\r\n"
383 "description for the file.\r\n"
384 "\r\n"
385 "To cancel your upload, press ^C a few times.\r\n\r\n");
386 session_flush(s);
387 session_pause_return(s, 0, "when you are ready...");
388
389 snprintf(upload_path, FILENAME_MAX, "%s:upload-%08lx%08lx",
390 folder->path, xorshift32(), xorshift32());
391
392 if (s->can_nomodem) {
393 ns = nomodem_receive(s, upload_path);
394 if (ns == NULL) {
395 xfree(&upload_path);
396 return 0;
397 }
398 } else {
399 zs = ZCreateReceiver(s, upload_path);
400 if (zs == NULL) {
401 xfree(&upload_path);
402 return 0;
403 }
404 zs->DoIACEscape = s->is_telnet;
405
406 ZInit(zs);
407 s->direct_output = true;
408 }
409
410 session_flush(s);
411 session_logf(s, "[%s] Receiving uploaded file to %s", folder->name,
412 upload_path);
413 while (!s->ending) {
414 if (s->can_nomodem) {
415 if (nomodem_timed_out(ns))
416 goto timed_out;
417 if (!nomodem_continue(ns))
418 break;
419 } else {
420 if (ZHaveTimedOut(zs)) {
421 ZTimeOutProc(zs);
422 goto timed_out;
423 }
424
425 if (!ZParse(zs))
426 break;
427 }
428
429 if (s->obuflen)
430 session_flush(s);
431
432 continue;
433 timed_out:
434 session_logf(s, "[%s] Transfer timed out, canceling", folder->name);
435 break;
436 }
437
438 /* flush the pipes */
439 s->obuflen = 0;
440 uthread_msleep(1000);
441 session_clear_input(s);
442 s->direct_output = false;
443
444 if (s->can_nomodem) {
445 file_size = ns->file_size;
446 if (ns->file) {
447 fclose(ns->file);
448 ns->file = NULL;
449 }
450 } else {
451 if (zs->file) {
452 fclose(zs->file);
453 zs->file = NULL;
454 }
455 file_size = zs->file_size;
456 }
457
458 session_printf(s, "\r\n\r\n");
459
460 if (stat(upload_path, &sb) != 0 || sb.st_size == 0) {
461 session_logf(s, "[%s] Failed receiving upload, no temp file",
462 folder->name);
463 session_printf(s, "Failed receiving file, aborting.\r\n");
464 session_pause_return(s, CONTROL_C, "to continue...");
465 if (s->can_nomodem) {
466 nomodem_finish(&ns);
467 } else {
468 ZDestroy(zs);
469 zs = NULL;
470 }
471 xfree(&upload_path);
472 return 0;
473 }
474
475 if (sb.st_size != file_size) {
476 session_logf(s, "[%s] Received uploaded file of size %ld but "
477 "supposed to be %ld, canceling", folder->name, sb.st_size,
478 file_size);
479 session_printf(s,
480 "Uploaded file expected to be {{B}}%ld{{/B}} byte%s, but "
481 "received\r\n"
482 "{{B}}%ld{{/B}} byte%s. Deleting file and canceling upload.\r\n",
483 file_size, file_size == 1 ? "" : "s",
484 sb.st_size, sb.st_size == 1 ? "" : "s");
485 session_pause_return(s, CONTROL_C, "to continue...");
486 if (s->can_nomodem) {
487 nomodem_finish(&ns);
488 } else {
489 ZDestroy(zs);
490 zs = NULL;
491 }
492 goto file_upload_cancel;
493 }
494
495 if (s->can_nomodem)
496 file_name = ns->file_name;
497 else
498 file_name = zs->file_name;
499
500 strlcpy(file.filename, file_name, sizeof(file.filename));
501 file.size = sb.st_size;
502
503 session_printf(s, "Successfully received file {{B}}%s{{/B}} of size "
504 "{{B}}%ld{{/B}} byte%s.\r\n",
505 file.filename, file.size, file.size == 1 ? "" : "s");
506 session_pause_return(s, '\r', "to continue...");
507
508 session_logf(s, "[%s] Received uploaded file %s of size %ld",
509 folder->name, file.filename, file.size);
510
511 if (s->can_nomodem) {
512 nomodem_finish(&ns);
513 } else {
514 ZDestroy(zs);
515 zs = NULL;
516 }
517
518 session_printf(s, "Calculating SHA1 checksum of file... ");
519 session_flush(s);
520 if (!folder_file_checksum(s, &file, upload_path)) {
521 session_printf(s, "failed reading file!\r\n");
522 session_flush(s);
523 goto file_upload_cancel;
524 }
525 session_output(s, "\r\n\r\n", 4);
526 session_flush(s);
527
528 if (folder_edit_file(s, folder, &file, upload_path))
529 goto file_upload_done;
530 else {
531 session_printf(s, "\r\nFailed saving file!\r\n");
532 session_flush(s);
533 }
534
535 file_upload_cancel:
536 session_printf(s, "\r\n");
537 session_flush(s);
538
539 if (upload_path[0] != '\0') {
540 CtoPstr(upload_path);
541 error = FSDelete(upload_path, 0);
542 PtoCstr(upload_path);
543 if (error) {
544 session_logf(s, "[%s] Failed deleting temporary uploaded "
545 "file %s: %d", folder->name, upload_path, error);
546 } else {
547 session_logf(s, "[%s] Canceled upload, deleted temp file %s",
548 folder->name, upload_path);
549 }
550 }
551
552 file_upload_done:
553 if (file.notes)
554 xfree(&file.notes);
555 if (upload_path != NULL)
556 xfree(&upload_path);
557
558 return file.id;
559 }
560
561 short
562 folder_file_view(struct session *s, struct folder *folder,
563 unsigned long id)
564 {
565 static const struct session_menu_option opts[] = {
566 { 'd', "Dd", "Download this file" },
567 { 'r', "Rr", "Remove this file" },
568 { 'e', "Ee", "Edit this file" },
569 { 'q', "QqXx", "Return to folder" },
570 { '?', "?", "List these options" },
571 };
572 static const char prompt_help[] =
573 "D:Download R:Delete E:Edit Q:Return ?:Help";
574 char time[32];
575 char prompt[6 + member_size(struct folder, name) + 1 +
576 member_size(struct folder_file, filename)];
577 char *path = NULL, *data, c;
578 struct folder_file file;
579 struct username_cache *uploader;
580 struct zmodem_session *zs;
581 struct nomodem_session *ns;
582 struct session_menu_option *dopts = NULL;
583 FILE *fp;
584 size_t size;
585 short cc, n, bret, ret = FILE_VIEW_RETURN_FIND;
586 bool done = false, show_help = false;
587
588 size = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, id, &data);
589 if (size == 0)
590 panic("failed fetching message %ld: %d", id,
591 bile_error(folder->bile));
592 bret = bile_unmarshall_object(folder->bile, folder_file_object_fields,
593 nfolder_file_object_fields, data, size, &file, sizeof(file), true);
594 xfree(&data);
595 if (bret == BILE_ERR_NO_MEMORY)
596 return ret;
597
598 dopts = xmalloc(sizeof(opts));
599 if (dopts == NULL)
600 return ret;
601 memcpy(dopts, opts, sizeof(opts));
602 if (!(s->user && (s->user->is_sysop ||
603 s->user->id == file.uploader_user_id))) {
604 /* disable deleting and editing */
605 for (n = 0; n < nitems(opts); n++) {
606 if (dopts[n].ret == 'r' || dopts[n].ret == 'e')
607 dopts[n].key[0] = '\0';
608 }
609 }
610
611 uploader = user_username(file.uploader_user_id);
612
613 strftime(time, sizeof(time), "%Y-%m-%d %H:%M:%S",
614 localtime(&file.time));
615
616 session_printf(s, "{{B}}Folder:{{/B}}{{#}} %s\r\n",
617 folder->name);
618 session_printf(s, "{{B}}File Name:{{/B}}{{#}} %s\r\n",
619 file.filename);
620 session_printf(s, "{{B}}Description:{{/B}}{{#}} %s\r\n",
621 file.description);
622 session_printf(s, "{{B}}File Size:{{/B}} %lu\r\n", file.size);
623 session_printf(s, "{{B}}File SHA1:{{/B}} %s\r\n", file.sha1_checksum);
624 session_printf(s, "{{B}}Uploaded:{{/B}} %s %s\r\n", time,
625 db->config.timezone);
626 session_printf(s, "{{B}}Uploaded By:{{/B}} %s\r\n",
627 uploader ? uploader->username : "(unknown)");
628 session_flush(s);
629
630 if (file.notes_size > 0) {
631 session_printf(s, "{{B}}Notes:{{/B}}\r\n");
632 session_output(s, file.notes, file.notes_size);
633 session_printf(s, "\r\n");
634 }
635
636 session_printf(s,
637 "\r\n[ Not viewing file contents, press 'd' to download. ]\r\n");
638 session_flush(s);
639
640 snprintf(prompt, sizeof(prompt), "Files:%s:%s", folder->name,
641 file.filename);
642
643 while (!done && !s->ending) {
644 c = session_menu(s, file.filename, prompt, (char *)prompt_help, dopts,
645 nitems(opts), show_help, NULL, NULL);
646 show_help = false;
647
648 switch (c) {
649 case 'd':
650 path = xmalloc(FILENAME_MAX);
651 if (path == NULL) {
652 done = true;
653 break;
654 }
655 snprintf(path, FILENAME_MAX, "%s:%s", folder->path,
656 file.filename);
657 fp = fopen(path, "rb");
658 if (!fp) {
659 session_logf(s, "[%s] Failed opening file %s",
660 folder->name, path);
661 session_printf(s,
662 "{{B}}Error:{{/B}} Failed opening file\r\n");
663 xfree(&path);
664 done = true;
665 break;
666 }
667
668 if (s->can_nomodem) {
669 ns = nomodem_send(s, fp, file.filename);
670 if (ns == NULL) {
671 xfree(&path);
672 done = true;
673 break;
674 }
675 } else {
676 zs = ZCreateSender(s, fp, file.filename);
677 if (zs == NULL) {
678 xfree(&path);
679 done = true;
680 break;
681 }
682 zs->DoIACEscape = s->is_telnet;
683 ZInit(zs);
684 s->direct_output = true;
685 }
686
687 session_logf(s, "[%s] Downloading file %s", folder->name,
688 file.filename);
689 for (;;) {
690 if (s->can_nomodem) {
691 if (nomodem_timed_out(ns))
692 goto timed_out;
693 if (!nomodem_continue(ns))
694 break;
695 } else {
696 if (ZHaveTimedOut(zs)) {
697 ZTimeOutProc(zs);
698 goto timed_out;
699 }
700
701 if (!ZParse(zs))
702 break;
703 }
704
705 if (s->obuflen)
706 session_flush(s);
707 continue;
708 timed_out:
709 session_logf(s, "[%s] Transfer timed out, canceling",
710 folder->name);
711 break;
712 }
713
714 /* flush the pipes */
715 s->obuflen = 0;
716 uthread_msleep(1000);
717 session_clear_input(s);
718
719 if (s->can_nomodem) {
720 nomodem_finish(&ns);
721 } else {
722 s->direct_output = false;
723 if (zs->file) {
724 fclose(zs->file);
725 zs->file = NULL;
726 }
727 ZDestroy(zs);
728 zs = NULL;
729 }
730 xfree(&path);
731
732 session_printf(s, "\r\n");
733 session_flush(s);
734 session_pause_return(s, CONTROL_C, "to continue...");
735 break;
736 case 'e':
737 if (!(s->user && (s->user->is_sysop ||
738 s->user->id == file.uploader_user_id))) {
739 session_printf(s, "Invalid option\r\n");
740 session_flush(s);
741 break;
742 }
743 path = xmalloc(FILENAME_MAX);
744 if (path == NULL) {
745 done = true;
746 break;
747 }
748 snprintf(path, FILENAME_MAX, "%s:%s", folder->path,
749 file.filename);
750 folder_edit_file(s, folder, &file, path);
751 xfree(&path);
752 break;
753 case 'r':
754 if (!(s->user && (s->user->is_sysop ||
755 s->user->id == file.uploader_user_id))) {
756 session_printf(s, "Invalid option\r\n");
757 session_flush(s);
758 break;
759 }
760
761 session_printf(s, "Are you sure you want to permanently "
762 "delete this file? [y/N] ");
763 session_flush(s);
764
765 cc = session_input_char(s);
766 if (cc == 'y' || c == 'Y') {
767 session_printf(s, "%c\r\n", cc);
768 session_flush(s);
769
770 folder_delete_file(s, folder, &file);
771
772 session_printf(s, "\r\n{{B}}File deleted!{{/B}}\r\n");
773 session_flush(s);
774 ret = FILE_VIEW_RETURN_FIND;
775 done = true;
776 } else {
777 session_printf(s, "\r\nFile not deleted.\r\n");
778 session_flush(s);
779 }
780 break;
781 case 'q':
782 done = true;
783 break;
784 case '?':
785 show_help = true;
786 break;
787 }
788 }
789
790 if (file.notes != NULL)
791 xfree(&file.notes);
792
793 xfree(&dopts);
794
795 return ret;
796 }
797
798 size_t
799 folder_find_file_ids(struct folder *folder, size_t *nfile_ids,
800 unsigned long **file_ids)
801 {
802 struct bile_object *o;
803 struct folder_file file;
804 struct file_name_map {
805 unsigned long id;
806 char filename[10]; /* only sorted to 10 character places */
807 } *name_map = NULL, tmp_map;
808 size_t n;
809 short i, j, ret;
810 char *data;
811
812 *nfile_ids = bile_count_by_type(folder->bile, FOLDER_FILE_RTYPE);
813 if (*nfile_ids == 0)
814 return 0;
815
816 name_map = xcalloc(*nfile_ids, sizeof(struct file_name_map));
817 if (name_map == NULL)
818 return 0;
819
820 for (n = 0; (o = bile_get_nth_of_type(folder->bile, n,
821 FOLDER_FILE_RTYPE)); n++) {
822 if (n >= *nfile_ids)
823 break;
824 if (bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, o->id,
825 &data) == 0)
826 break;
827 ret = bile_unmarshall_object(folder->bile, folder_file_object_fields,
828 nfolder_file_object_fields, data, o->size, &file, sizeof(file),
829 false);
830 xfree(&o);
831 xfree(&data);
832 if (ret == 0 && bile_error(folder->bile) == BILE_ERR_NO_MEMORY) {
833 xfree(&name_map);
834 return 0;
835 }
836
837 name_map[n].id = file.id;
838 strlcpy(name_map[n].filename, file.filename,
839 sizeof(name_map[n].filename));
840 }
841
842 /* sort by filename */
843 for (i = 1; i < *nfile_ids; i++) {
844 for (j = i; j > 0; j--) {
845 if (strcasecmp(name_map[j].filename,
846 name_map[j - 1].filename) > 0)
847 break;
848 tmp_map = name_map[j];
849 name_map[j] = name_map[j - 1];
850 name_map[j - 1] = tmp_map;
851 }
852 }
853
854 *file_ids = xcalloc(sizeof(long), *nfile_ids);
855 if (*file_ids == NULL) {
856 xfree(&name_map);
857 return 0;
858 }
859 for (i = 0; i < *nfile_ids; i++)
860 (*file_ids)[i] = name_map[i].id;
861
862 xfree(&name_map);
863
864 done:
865 return *nfile_ids;
866 }
867
868 bool
869 folder_file_save(struct folder *folder, struct folder_file *file,
870 char *temp_path)
871 {
872 char *new_name = NULL;
873 short ret;
874 char *data;
875 size_t size;
876 bool need_move = false;
877
878 if (!file->id) {
879 need_move = true;
880 file->id = bile_next_id(folder->bile, FOLDER_FILE_RTYPE);
881 }
882 if (!file->time)
883 file->time = Time;
884
885 ret = bile_marshall_object(folder->bile, folder_file_object_fields,
886 nfolder_file_object_fields, file, &data, &size);
887 if (ret != 0 || size == 0) {
888 warn("failed to marshall new file object");
889 file->id = 0;
890 return false;
891 }
892 if (bile_write(folder->bile, FOLDER_FILE_RTYPE, file->id, data,
893 size) != size) {
894 warn("bile_write of new file failed! %d", bile_error(folder->bile));
895 file->id = 0;
896 xfree(&data);
897 return false;
898 }
899 xfree(&data);
900
901 bile_flush(folder->bile, true);
902
903 if (need_move) {
904 new_name = xmalloc(FILENAME_MAX);
905 if (new_name == NULL)
906 return false;
907 snprintf(new_name, FILENAME_MAX, "%s:%s", folder->path,
908 file->filename);
909 CtoPstr(temp_path);
910 CtoPstr(new_name);
911 ret = Rename(temp_path, 0, new_name);
912 PtoCstr(temp_path);
913 PtoCstr(new_name);
914 if (ret != 0) {
915 warn("FSRename(%s, 0, %s) failed: %d", temp_path, new_name,
916 ret);
917 bile_delete(folder->bile, FOLDER_FILE_RTYPE, file->id,
918 BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE);
919 file->id = 0;
920 xfree(&new_name);
921 return false;
922 }
923
924 xfree(&new_name);
925 }
926
927 return true;
928 }
929
930 void
931 folder_delete_file(struct session *s, struct folder *folder,
932 struct folder_file *file)
933 {
934 char *path = NULL;
935 short ret;
936
937 bile_delete(folder->bile, FOLDER_FILE_RTYPE, file->id,
938 BILE_DELETE_FLAG_ZERO | BILE_DELETE_FLAG_PURGE);
939 bile_flush(folder->bile, true);
940
941 path = xmalloc(FILENAME_MAX);
942 if (path == NULL)
943 return;
944 snprintf(path, FILENAME_MAX, "%s:%s", folder->path, file->filename);
945 CtoPstr(path);
946 ret = FSDelete(path, 0);
947 PtoCstr(path);
948
949 if (ret != 0)
950 session_logf(s, "[%s] Failed deleting %s: %d", folder->name,
951 path, ret);
952
953 xfree(&path);
954 if (file->notes != NULL)
955 xfree(&file->notes);
956
957 session_logf(s, "[%s] Deleted file %s (%ld)", folder->name,
958 file->filename, file->id);
959 }
960
961 bool
962 folder_file_valid_filename(struct session *session,
963 struct folder *folder, struct folder_file *file, char **error)
964 {
965 struct bile_object *o;
966 struct folder_file tfile;
967 size_t len, n;
968 short ret;
969 char *data;
970 char c;
971
972 if (file->filename[0] == '\0') {
973 *error = xstrdup("filename cannot be empty");
974 return false;
975 }
976
977 len = strlen(file->filename);
978 if (len > FOLDER_FILE_FILENAME_LENGTH) {
979 *error = xmalloc(61);
980 if (*error)
981 snprintf(*error, 60, "filename cannot be more than %d "
982 "characters", FOLDER_FILE_FILENAME_LENGTH);
983 return false;
984 }
985
986 for (n = 0; n < len; n++) {
987 c = file->filename[n];
988
989 if (n == 0 && c == '.') {
990 *error = xstrdup("filename cannot start with a dot");
991 return false;
992 }
993
994 if (n == len - 1 && c == ' ') {
995 *error = xstrdup("filename cannot end with a space");
996 return false;
997 }
998
999 if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
1000 (c >= 'a' && c <= 'z') || c == '_' || c == '-' || c == '+' ||
1001 c == '*' || c == ' ' || c == '.' || c == ',')) {
1002 *error = xmalloc(61);
1003 if (*error)
1004 snprintf(*error, 60, "filename cannot contain '%c' "
1005 "character", c);
1006 return false;
1007 }
1008 }
1009
1010 for (n = 0; (o = bile_get_nth_of_type(folder->bile, n,
1011 FOLDER_FILE_RTYPE)); n++) {
1012 if (o->id == file->id) {
1013 xfree(&o);
1014 continue;
1015 }
1016
1017 ret = bile_read_alloc(folder->bile, FOLDER_FILE_RTYPE, o->id, &data);
1018 if (ret == 0) {
1019 xfree(&o);
1020 return false;
1021 }
1022 ret = bile_unmarshall_object(folder->bile, folder_file_object_fields,
1023 nfolder_file_object_fields, data, o->size, &tfile, sizeof(tfile),
1024 false);
1025 xfree(&data);
1026 xfree(&o);
1027 if (ret == BILE_ERR_NO_MEMORY)
1028 return false;
1029
1030 if (strcasecmp(file->filename, tfile.filename) == 0) {
1031 *error = xstrdup("filename is already taken");
1032 return false;
1033 }
1034 }
1035 if (bile_error(folder->bile) == BILE_ERR_NO_MEMORY)
1036 return false;
1037
1038 return true;
1039 }
1040
1041 bool
1042 folder_edit_file(struct session *s, struct folder *folder,
1043 struct folder_file *file, char *file_path)
1044 {
1045 struct username_cache *uploader;
1046 char *tmp, *errorstr;
1047 unsigned short c;
1048
1049 uploader = user_username(file->uploader_user_id);
1050
1051 session_printf(s, "{{B}}Uploaded By:{{/B}} %s\r\n",
1052 uploader ? uploader->username : "(unknown)");
1053 session_printf(s, "{{B}}Folder:{{/B}} %s\r\n",
1054 folder->name);
1055 session_printf(s, "{{B}}SHA1 Checksum:{{/B}} %s\r\n",
1056 file->sha1_checksum);
1057 session_printf(s, "{{B}}File Size:{{/B}} %ld\r\n",
1058 file->size);
1059 session_flush(s);
1060
1061 file_annotate:
1062 for (;;) {
1063 session_printf(s, "{{B}}File Name:{{/B}} ");
1064 session_flush(s);
1065
1066 if (file->id) {
1067 /* TODO: allow renaming and do FSRename */
1068 session_printf(s, "{{#}}%s\r\n", file->filename);
1069 } else {
1070 tmp = session_field_input(s, FOLDER_FILE_FILENAME_LENGTH,
1071 FOLDER_FILE_FILENAME_LENGTH - 1, file->filename, false, 0);
1072 if (tmp == NULL)
1073 return false;
1074 strlcpy(file->filename, tmp, sizeof(file->filename));
1075 xfree(&tmp);
1076 session_output(s, "\r\n", 2);
1077 session_flush(s);
1078
1079 rtrim(file->filename, "\r\n\t ");
1080
1081 if (!folder_file_valid_filename(s, folder, file, &errorstr)) {
1082 session_printf(s, "{{B}}Error:{{/B}} %s\r\n", errorstr);
1083 xfree(&errorstr);
1084 continue;
1085 }
1086 }
1087
1088 break;
1089 }
1090
1091 for (;;) {
1092 session_printf(s, "{{B}}Short Description:{{/B}} ");
1093 session_flush(s);
1094
1095 tmp = session_field_input(s, FOLDER_FILE_DESCR_LENGTH,
1096 FOLDER_FILE_DESCR_LENGTH - 1, file->description, false, 0);
1097 if (tmp == NULL)
1098 return false;
1099 strlcpy(file->description, tmp, sizeof(file->description));
1100 xfree(&tmp);
1101 session_output(s, "\r\n", 2);
1102 session_flush(s);
1103
1104 rtrim(file->description, "\r\n\t ");
1105
1106 if (file->description[0] == '\0') {
1107 session_printf(s, "{{B}}Error:{{/B}} File "
1108 "description cannot be blank (^C to cancel)\r\n");
1109 session_flush(s);
1110 continue;
1111 }
1112
1113 break;
1114 }
1115
1116 for (;;) {
1117 session_printf(s,
1118 "{{B}}Notes (Optional, ^D when finished):{{/B}}\r\n");
1119 session_flush(s);
1120
1121 tmp = session_field_input(s, 2048, s->terminal_columns - 1,
1122 file->notes, true, 0);
1123 if (file->notes != NULL)
1124 xfree(&file->notes);
1125 file->notes = tmp;
1126 session_output(s, "\r\n", 2);
1127 session_flush(s);
1128 if (file->notes == NULL) {
1129 file->notes_size = 0;
1130 break;
1131 }
1132
1133 rtrim(file->notes, "\r\n\t ");
1134
1135 if (file->notes[0] == '\0') {
1136 xfree(&file->notes);
1137 file->notes = NULL;
1138 file->notes_size = 0;
1139 break;
1140 }
1141 file->notes_size = strlen(file->notes) + 1;
1142 break;
1143 }
1144
1145 for (;;) {
1146 session_printf(s, "\r\n{{B}}(S){{/B}}ave, "
1147 "{{B}}(E){{/B}}dit again, ");
1148 if (file->id && s->user->is_sysop)
1149 session_printf(s, "Re-c{{B}}(h){{/B}}ecksum, ");
1150 session_printf(s, "or {{B}}(C){{/B}}ancel? ");
1151 session_flush(s);
1152
1153 c = session_input_char(s);
1154 if (c == 0 || s->ending)
1155 return false;
1156
1157 switch (c) {
1158 case 'h':
1159 case 'H':
1160 if (s->user->is_sysop) {
1161 session_printf(s, "%c\r\n", c);
1162 session_flush(s);
1163
1164 session_printf(s, "Calculating SHA1 checksum... ");
1165 session_flush(s);
1166
1167 if (!folder_file_checksum(s, file, file_path)) {
1168 session_printf(s, "failed reading %s!\r\n", file_path);
1169 session_flush(s);
1170 return false;
1171 }
1172 session_printf(s, "\r\nNew checksum: %s\r\n",
1173 file->sha1_checksum);
1174 session_flush(s);
1175 }
1176 break;
1177 case 's':
1178 case 'S':
1179 case 'y':
1180 session_printf(s, "%c\r\n", c);
1181 session_flush(s);
1182 /* FALLTHROUGH */
1183 case '\n':
1184 case '\r':
1185 /* save */
1186 session_printf(s, "Saving file... ");
1187 session_flush(s);
1188
1189 if (folder_file_save(folder, file, file_path)) {
1190 session_logf(s, "[%s] Saved file %s", folder->name,
1191 file->filename);
1192 session_printf(s, "done\r\n");
1193 session_flush(s);
1194 return true;
1195 }
1196
1197 session_printf(s, "failed!\r\n");
1198 session_flush(s);
1199 return false;
1200 case 'e':
1201 case 'E':
1202 session_printf(s, "%c\r\n", c);
1203 session_flush(s);
1204 goto file_annotate;
1205 case 'c':
1206 case 'C':
1207 session_printf(s, "%c\r\n", c);
1208 session_flush(s);
1209 return false;
1210 case CONTROL_C:
1211 return false;
1212 }
1213 }
1214
1215 return false;
1216 }
1217
1218 bool
1219 folder_file_checksum(struct session *s, struct folder_file *file,
1220 char *file_path)
1221 {
1222 SHA1_CTX sha1;
1223 FILE *fp;
1224 char *data;
1225 char fmt[12];
1226 size_t rsize, size, dg, olen, tsize;
1227
1228 data = xmalloc(512);
1229 if (data == NULL)
1230 return false;
1231
1232 SHA1Init(&sha1);
1233 fp = fopen(file_path, "rb");
1234 if (fp == NULL) {
1235 xfree(&data);
1236 return false;
1237 }
1238
1239 tsize = file->size;
1240 dg = 1;
1241 while (tsize >= 10) {
1242 dg++;
1243 tsize /= 10;
1244 }
1245 /* 123456 -> "% 6lu/%lu" */
1246 snprintf(fmt, sizeof(fmt), "%% %lulu/%%lu", dg);
1247
1248 rsize = 0;
1249 olen = session_printf(s, fmt, rsize, file->size);
1250 session_flush(s);
1251 while (!feof(fp)) {
1252 size = fread(data, 1, 512, fp);
1253 rsize += size;
1254 SHA1Update(&sha1, (const u_int8_t *)data, size);
1255 if (rsize % 2048 == 0)
1256 uthread_yield();
1257 if (rsize % 10240 == 0 || rsize == file->size) {
1258 session_printf(s, "%s", ansi(s, ANSI_BACK_N, (short)olen,
1259 ANSI_END));
1260 session_printf(s, fmt, rsize, file->size);
1261 session_flush(s);
1262 }
1263 }
1264 fclose(fp);
1265 xfree(&data);
1266
1267 SHA1End(&sha1, (char *)&file->sha1_checksum);
1268
1269 if (rsize != file->size)
1270 return false;
1271
1272 return true;
1273 }