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 | } |