Download
jcs
/subtext
/db.c
(View History)
jcs db: Free bile object in db_cache_boards | Latest amendment: 591 on 2024-02-16 |
1 | /* |
2 | * Copyright (c) 2021-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 <stdio.h> |
18 | #include <string.h> |
19 | #include <time.h> |
20 | |
21 | #include "subtext.h" |
22 | #include "board.h" |
23 | #include "bile.h" |
24 | #include "db.h" |
25 | #include "folder.h" |
26 | #include "logger.h" |
27 | #include "mail.h" |
28 | #include "main_menu.h" |
29 | #include "user.h" |
30 | #include "util.h" |
31 | |
32 | struct struct_field config_fields[] = { |
33 | { "BBS Name", CONFIG_TYPE_STRING, |
34 | offsetof(struct config, name), |
35 | 1, member_size(struct config, name) }, |
36 | { "Phone Number", CONFIG_TYPE_STRING, |
37 | offsetof(struct config, phone_number), |
38 | 1, member_size(struct config, phone_number) }, |
39 | { "Location", CONFIG_TYPE_STRING, |
40 | offsetof(struct config, location), |
41 | 1, member_size(struct config, location) }, |
42 | { "Timezone (Abbrev)", CONFIG_TYPE_STRING, |
43 | offsetof(struct config, timezone), |
44 | 1, member_size(struct config, timezone) }, |
45 | { "Timezone (UTC Offset)", CONFIG_TYPE_SHORT, |
46 | offsetof(struct config, timezone_utcoff), |
47 | -1200, 1400 }, |
48 | { "Allow Open Signup", CONFIG_TYPE_BOOLEAN, |
49 | offsetof(struct config, open_signup), |
50 | 0, 0 }, |
51 | |
52 | { "Hostname", CONFIG_TYPE_STRING, |
53 | offsetof(struct config, hostname), |
54 | 1, member_size(struct config, hostname) }, |
55 | { "Telnet Port", CONFIG_TYPE_SHORT, |
56 | offsetof(struct config, telnet_port), |
57 | 0, 65535, |
58 | CONFIG_REQUIRES_TELNET_REINIT }, |
59 | |
60 | { "Telnet Trusted Proxy IP", CONFIG_TYPE_IP, |
61 | offsetof(struct config, trusted_proxy_ip), |
62 | 0, 1, |
63 | CONFIG_REQUIRES_TELNET_REINIT }, |
64 | { "Telnet Trusted Proxy UDP Port", CONFIG_TYPE_SHORT, |
65 | offsetof(struct config, trusted_proxy_udp_port), |
66 | 0, 65535, |
67 | CONFIG_REQUIRES_TELNET_REINIT }, |
68 | { "IP Geolocation Database Path", CONFIG_TYPE_STRING, |
69 | offsetof(struct config, ipdb_path), |
70 | 0, member_size(struct config, ipdb_path), |
71 | CONFIG_REQUIRES_IPDB_REINIT }, |
72 | |
73 | { "Syslog Server IP", CONFIG_TYPE_IP, |
74 | offsetof(struct config, syslog_ip), |
75 | 0, 1, |
76 | CONFIG_REQUIRES_SYSLOG_REINIT }, |
77 | |
78 | { "Modem Port", CONFIG_TYPE_SHORT, |
79 | offsetof(struct config, modem_port), |
80 | 0, 2, |
81 | CONFIG_REQUIRES_SERIAL_REINIT }, |
82 | { "Modem Port Speed", CONFIG_TYPE_LONG, |
83 | offsetof(struct config, modem_speed), |
84 | 300, 115200, |
85 | CONFIG_REQUIRES_SERIAL_REINIT }, |
86 | { "Modem Bits/Parity/Stop", CONFIG_TYPE_STRING, |
87 | offsetof(struct config, modem_parity), |
88 | 1, member_size(struct config, modem_parity), |
89 | CONFIG_REQUIRES_SERIAL_REINIT }, |
90 | { "Modem Init String", CONFIG_TYPE_STRING, |
91 | offsetof(struct config, modem_init), |
92 | 1, member_size(struct config, modem_init), |
93 | CONFIG_REQUIRES_SERIAL_REINIT }, |
94 | { "Modem Answer After Rings", CONFIG_TYPE_SHORT, |
95 | offsetof(struct config, modem_rings), |
96 | 1, 100 }, |
97 | |
98 | { "Max Idle Minutes", CONFIG_TYPE_SHORT, |
99 | offsetof(struct config, max_idle_minutes), |
100 | 0, USHRT_MAX }, |
101 | { "Max Sysop Idle Minutes", CONFIG_TYPE_SHORT, |
102 | offsetof(struct config, max_sysop_idle_minutes), |
103 | 0, USHRT_MAX }, |
104 | { "Max Login Seconds", CONFIG_TYPE_SHORT, |
105 | offsetof(struct config, max_login_seconds), |
106 | 1, USHRT_MAX }, |
107 | |
108 | { "Screen Blanker Idle Seconds", CONFIG_TYPE_SHORT, |
109 | offsetof(struct config, blanker_idle_seconds), |
110 | 0, USHRT_MAX }, |
111 | { "Screen Blanker Runtime Seconds", CONFIG_TYPE_SHORT, |
112 | offsetof(struct config, blanker_runtime_seconds), |
113 | 1, USHRT_MAX }, |
114 | |
115 | { "Prune Session Logs After N Days", CONFIG_TYPE_SHORT, |
116 | offsetof(struct config, session_log_prune_days), |
117 | 0, USHRT_MAX }, |
118 | { "Prune Mail After N Days", CONFIG_TYPE_SHORT, |
119 | offsetof(struct config, mail_prune_days), |
120 | 0, USHRT_MAX }, |
121 | |
122 | { "FTN Network Name", CONFIG_TYPE_STRING, |
123 | offsetof(struct config, ftn_network), |
124 | 0, member_size(struct config, ftn_network), |
125 | CONFIG_REQUIRES_BINKP_REINIT }, |
126 | { "FTN Local Node Address", CONFIG_TYPE_STRING, |
127 | offsetof(struct config, ftn_node_addr), |
128 | 0, member_size(struct config, ftn_node_addr), |
129 | CONFIG_REQUIRES_BINKP_REINIT }, |
130 | { "FTN Hub Node Address", CONFIG_TYPE_STRING, |
131 | offsetof(struct config, ftn_hub_addr), |
132 | 0, member_size(struct config, ftn_hub_addr), |
133 | CONFIG_REQUIRES_BINKP_REINIT }, |
134 | { "FTN Hub Packet Password", CONFIG_TYPE_PASSWORD, |
135 | offsetof(struct config, ftn_hub_pkt_password), |
136 | 0, member_size(struct config, ftn_hub_pkt_password) }, |
137 | { "FTN Hub Binkp Hostname", CONFIG_TYPE_STRING, |
138 | offsetof(struct config, binkp_hostname), |
139 | 0, member_size(struct config, binkp_hostname), |
140 | CONFIG_REQUIRES_BINKP_REINIT }, |
141 | { "FTN Hub Binkp Port", CONFIG_TYPE_SHORT, |
142 | offsetof(struct config, binkp_port), |
143 | 0, 65535 }, |
144 | { "FTN Hub Binkp Password", CONFIG_TYPE_PASSWORD, |
145 | offsetof(struct config, binkp_password), |
146 | 0, member_size(struct config, binkp_password) }, |
147 | { "FTN Hub Binkp Poll Seconds", CONFIG_TYPE_LONG, |
148 | offsetof(struct config, binkp_interval_seconds), |
149 | 1, LONG_MAX }, |
150 | { "FTN Delete Packets After Processing", CONFIG_TYPE_BOOLEAN, |
151 | offsetof(struct config, binkp_delete_done), |
152 | 0, 0 }, |
153 | { "FTN Max Tossed Message Size", CONFIG_TYPE_LONG, |
154 | offsetof(struct config, ftn_max_tossed_message_size), |
155 | 0, LONG_MAX }, |
156 | }; |
157 | size_t nconfig_fields = nitems(config_fields); |
158 | |
159 | struct db * db_init(Str255 file, short vrefnum, struct bile *bile); |
160 | short db_migrate(struct db *tdb, short is_new, Str255 basepath); |
161 | void db_config_load(struct db *tdb); |
162 | const char * db_view_filename(short id); |
163 | |
164 | struct db * |
165 | db_open(Str255 file, short vrefnum, bool ignore_last) |
166 | { |
167 | Str255 filepath; |
168 | struct stat sb; |
169 | Point pt = { 75, 100 }; |
170 | SFReply reply = { 0 }; |
171 | SFTypeList types; |
172 | StringHandle lastfileh; |
173 | struct db *ret; |
174 | |
175 | if (file[0]) { |
176 | getpath(vrefnum, file, filepath, true); |
177 | if (FStat(filepath, &sb) == 0) |
178 | return db_init(file, vrefnum, NULL); |
179 | |
180 | warn("Failed to open %s", PtoCstr(file)); |
181 | } |
182 | |
183 | if (!ignore_last && (lastfileh = GetString(STR_LAST_DB))) { |
184 | HLock(lastfileh); |
185 | memcpy(filepath, *lastfileh, sizeof(filepath)); |
186 | HUnlock(lastfileh); |
187 | ReleaseResource(lastfileh); |
188 | if (FStat(filepath, &sb) == 0) { |
189 | ret = db_init(filepath, 0, NULL); |
190 | return ret; |
191 | } |
192 | } |
193 | |
194 | /* no file passed, no last file, prompt */ |
195 | types[0] = DB_TYPE; |
196 | SFGetFile(pt, NULL, NULL, 1, &types, NULL, &reply); |
197 | if (reply.good) { |
198 | ret = db_init(reply.fName, reply.vRefNum, NULL); |
199 | return ret; |
200 | } |
201 | |
202 | return db_create(); |
203 | } |
204 | |
205 | struct db * |
206 | db_create(void) |
207 | { |
208 | Point pt = { 75, 100 }; |
209 | SFReply reply; |
210 | struct bile *bile; |
211 | short error = 0; |
212 | |
213 | SFPutFile(pt, "\pCreate new Subtext DB:", "\p", NULL, &reply); |
214 | if (!reply.good) |
215 | return NULL; |
216 | |
217 | bile = bile_create(reply.fName, reply.vRefNum, SUBTEXT_CREATOR, |
218 | DB_TYPE); |
219 | if (bile == NULL && bile_error(NULL) == dupFNErr) { |
220 | error = FSDelete(reply.fName, reply.vRefNum); |
221 | if (error) |
222 | panic("Failed to re-create file %s: %d", PtoCstr(reply.fName), |
223 | error); |
224 | bile = bile_create(reply.fName, reply.vRefNum, SUBTEXT_CREATOR, |
225 | DB_TYPE); |
226 | } |
227 | |
228 | if (bile == NULL) |
229 | panic("Failed to create file %s: %d", PtoCstr(reply.fName), error); |
230 | |
231 | return db_init(reply.fName, reply.vRefNum, bile); |
232 | } |
233 | |
234 | struct db * |
235 | db_init(Str255 path, short vrefnum, struct bile *bile) |
236 | { |
237 | Str255 fullpath, newfullpath, viewsdir; |
238 | struct db *tdb; |
239 | Handle lastfileh; |
240 | short was_new, error; |
241 | long dirid; |
242 | |
243 | was_new = (bile != NULL); |
244 | |
245 | if (bile == NULL) { |
246 | bile = bile_open(path, vrefnum); |
247 | if (bile == NULL) { |
248 | if (ask("Attempt recovery with backup map?")) { |
249 | bile = bile_open_recover_map(path, vrefnum); |
250 | if (bile == NULL) |
251 | panic("Failed to recover DB file %s: %d", |
252 | PtoCstr(path), bile_error(NULL)); |
253 | } else { |
254 | panic("Failed to open DB file %s: %d", PtoCstr(path), |
255 | bile_error(NULL)); |
256 | } |
257 | } |
258 | } |
259 | |
260 | /* we got this far, store it as the last-accessed file */ |
261 | if (vrefnum == 0) |
262 | memcpy(&fullpath, path, sizeof(fullpath)); |
263 | else |
264 | getpath(vrefnum, path, fullpath, true); |
265 | lastfileh = Get1Resource('STR ', STR_LAST_DB); |
266 | if (lastfileh) |
267 | xSetHandleSize(lastfileh, fullpath[0] + 1); |
268 | else |
269 | lastfileh = xNewHandle(fullpath[0] + 1); |
270 | HLock(lastfileh); |
271 | memcpy(*lastfileh, fullpath, fullpath[0] + 1); |
272 | HUnlock(lastfileh); |
273 | if (HomeResFile(lastfileh) == -1) |
274 | AddResource(lastfileh, 'STR ', STR_LAST_DB, "\pSTR_LAST_DB"); |
275 | else |
276 | ChangedResource(lastfileh); |
277 | WriteResource(lastfileh); |
278 | DetachResource(lastfileh); |
279 | |
280 | tdb = xmalloczero(sizeof(struct db)); |
281 | if (tdb == NULL) |
282 | panic("Can't create db"); |
283 | tdb->bile = bile; |
284 | |
285 | /* create views directory if it doesn't exist */ |
286 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, viewsdir, |
287 | false) != 0) |
288 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
289 | PtoCstr(viewsdir); |
290 | strlcat((char *)viewsdir, ":views", sizeof(Str255)); |
291 | CtoPstr(viewsdir); |
292 | if (!FIsDir(viewsdir)) { |
293 | error = DirCreate(tdb->bile->vrefnum, 0, viewsdir, &dirid); |
294 | if (error) |
295 | panic("Failed creating %s: %d", PtoCstr(viewsdir), error); |
296 | } |
297 | |
298 | if (db_migrate(tdb, was_new, fullpath) != 0) { |
299 | bile_close(tdb->bile); |
300 | xfree(&tdb); |
301 | return NULL; |
302 | } |
303 | |
304 | if (!was_new) |
305 | db_config_load(tdb); |
306 | |
307 | memcpy(newfullpath, fullpath, sizeof(newfullpath)); |
308 | PtoCstr(newfullpath); |
309 | strlcat((char *)&newfullpath, "-sessions", sizeof(newfullpath)); |
310 | CtoPstr(newfullpath); |
311 | |
312 | tdb->sessions_bile = bile_open(newfullpath, vrefnum); |
313 | if (tdb->sessions_bile == NULL) { |
314 | tdb->sessions_bile = bile_create(newfullpath, vrefnum, |
315 | SUBTEXT_CREATOR, SL_TYPE); |
316 | if (tdb->sessions_bile == NULL) |
317 | panic("Couldn't create %s: %d", PtoCstr(newfullpath), |
318 | bile_error(NULL)); |
319 | } |
320 | |
321 | memcpy(newfullpath, fullpath, sizeof(newfullpath)); |
322 | PtoCstr(newfullpath); |
323 | strlcat((char *)&newfullpath, "-mail", sizeof(newfullpath)); |
324 | CtoPstr(newfullpath); |
325 | |
326 | tdb->mail_bile = bile_open(newfullpath, vrefnum); |
327 | if (tdb->mail_bile == NULL) { |
328 | tdb->mail_bile = bile_create(newfullpath, vrefnum, |
329 | SUBTEXT_CREATOR, MAIL_SPOOL_TYPE); |
330 | if (tdb->mail_bile == NULL) |
331 | panic("Couldn't create %s: %d", PtoCstr(newfullpath), |
332 | bile_error(NULL)); |
333 | } |
334 | |
335 | return tdb; |
336 | } |
337 | |
338 | void |
339 | db_close(struct db *tdb) |
340 | { |
341 | short n; |
342 | |
343 | bile_close(tdb->bile); |
344 | xfree(&tdb->bile); |
345 | |
346 | if (tdb->sessions_bile != NULL) { |
347 | bile_close(tdb->sessions_bile); |
348 | xfree(&tdb->sessions_bile); |
349 | } |
350 | |
351 | if (tdb->mail_bile != NULL) { |
352 | bile_close(tdb->mail_bile); |
353 | xfree(&tdb->mail_bile); |
354 | } |
355 | |
356 | if (tdb->boards) { |
357 | for (n = 0; n < tdb->nboards; n++) { |
358 | if (tdb->boards[n].bile) |
359 | bile_close(tdb->boards[n].bile); |
360 | } |
361 | xfree(&tdb->boards); |
362 | } |
363 | |
364 | if (tdb->folders) { |
365 | for (n = 0; n < tdb->nfolders; n++) { |
366 | if (tdb->folders[n].bile) |
367 | bile_close(tdb->folders[n].bile); |
368 | } |
369 | xfree(&tdb->folders); |
370 | } |
371 | |
372 | xfree(&tdb); |
373 | } |
374 | |
375 | short |
376 | db_migrate(struct db *tdb, short is_new, Str255 fullpath) |
377 | { |
378 | struct user *suser; |
379 | struct db *olddb; |
380 | char ver; |
381 | |
382 | if (is_new) { |
383 | ver = DB_CUR_VERS; |
384 | |
385 | /* setup some defaults */ |
386 | sprintf(tdb->config.name, "Example Subtext BBS"); |
387 | sprintf(tdb->config.phone_number, "(555) 867-5309"); |
388 | sprintf(tdb->config.location, "Springfield"); |
389 | sprintf(tdb->config.hostname, "bbs.example.com"); |
390 | sprintf(tdb->config.timezone, "CT"); |
391 | tdb->config.modem_speed = 9600; |
392 | sprintf(tdb->config.modem_init, "ATV1S0=2&D2"); |
393 | sprintf(tdb->config.modem_parity, "8N1"); |
394 | tdb->config.modem_rings = 1; |
395 | tdb->config.max_idle_minutes = 5; |
396 | tdb->config.max_sysop_idle_minutes = 60; |
397 | tdb->config.max_login_seconds = 90; |
398 | tdb->config.blanker_idle_seconds = (60 * 60); |
399 | tdb->config.blanker_runtime_seconds = 30; |
400 | tdb->config.session_log_prune_days = 21; |
401 | tdb->config.binkp_port = 24554; |
402 | tdb->config.binkp_interval_seconds = (60 * 60 * 3); |
403 | tdb->config.mail_prune_days = 180; |
404 | tdb->config.ftn_max_tossed_message_size = 16 * 1024; |
405 | |
406 | db_config_save(tdb); |
407 | |
408 | /* create a default sysop user */ |
409 | suser = xmalloczero(sizeof(struct user)); |
410 | if (suser == NULL) |
411 | panic("Can't allocate new user"); |
412 | strncpy(suser->username, "sysop", sizeof(suser->username)); |
413 | suser->created_at = Time; |
414 | suser->is_enabled = DB_TRUE; |
415 | user_set_password(suser, "p4ssw0rd"); |
416 | suser->is_sysop = DB_TRUE; |
417 | /* user_save assumes db is already set */ |
418 | olddb = db; |
419 | db = tdb; |
420 | user_save(suser); |
421 | xfree(&suser); |
422 | user_cache_usernames(); |
423 | db = olddb; |
424 | } else { |
425 | if (bile_read(tdb->bile, DB_VERS_RTYPE, 1, &ver, 1) != 1) |
426 | ver = 1; |
427 | |
428 | if (ver == DB_CUR_VERS) |
429 | return 0; |
430 | |
431 | if (ask("Migrate this database from version %d to %d to open it?", |
432 | ver, DB_CUR_VERS) != ASK_YES) |
433 | return -1; |
434 | } |
435 | |
436 | /* per-version migrations */ |
437 | while (ver < DB_CUR_VERS) { |
438 | progress("Migrating from version %d to %d...", (short)ver, |
439 | (short)(ver + 1)); |
440 | |
441 | if (ver < 18) |
442 | panic("This Subtext database is too old to upgrade. Please " |
443 | "run an older Subtext release first to upgrade it."); |
444 | |
445 | switch (ver) { |
446 | case 18: { |
447 | /* 18->19, ipdb_path, add session_log.location */ |
448 | struct config new_config = { 0 }; |
449 | Str255 newfullpath; |
450 | struct bile *sessions_bile; |
451 | size_t nids, n, size; |
452 | unsigned long *ids; |
453 | char *data; |
454 | |
455 | bile_read(tdb->bile, DB_CONFIG_RTYPE, 1, (char *)&new_config, |
456 | sizeof(new_config)); |
457 | |
458 | new_config.ipdb_path[0] = '\0'; |
459 | |
460 | bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &new_config, |
461 | sizeof(new_config)); |
462 | |
463 | /* migrate session log entries */ |
464 | memcpy(&newfullpath, fullpath, sizeof(newfullpath)); |
465 | PtoCstr(newfullpath); |
466 | strlcat((char *)&newfullpath, "-sessions", sizeof(newfullpath)); |
467 | CtoPstr(newfullpath); |
468 | |
469 | sessions_bile = bile_open(newfullpath, tdb->bile->vrefnum); |
470 | if (sessions_bile == NULL) |
471 | /* nothing to migrate */ |
472 | break; |
473 | |
474 | nids = bile_ids_by_type(sessions_bile, SL_LOG_RTYPE, &ids); |
475 | for (n = 0; n < nids; n++) { |
476 | size = bile_read_alloc(sessions_bile, SL_LOG_RTYPE, |
477 | ids[n], &data); |
478 | size += member_size(struct session_log, location); |
479 | if (bile_resize(sessions_bile, SL_LOG_RTYPE, |
480 | ids[n], size) != size) |
481 | panic("failed resizing session log %ld", ids[n]); |
482 | } |
483 | |
484 | bile_flush(sessions_bile, true); |
485 | xfree(&ids); |
486 | bile_close(sessions_bile); |
487 | break; |
488 | } |
489 | case 19: { |
490 | /* 19->20, modem rings */ |
491 | struct config new_config = { 0 }; |
492 | |
493 | bile_read(tdb->bile, DB_CONFIG_RTYPE, 1, (char *)&new_config, |
494 | sizeof(new_config)); |
495 | |
496 | new_config.modem_rings = 1; |
497 | |
498 | bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &new_config, |
499 | sizeof(new_config)); |
500 | break; |
501 | } |
502 | case 20: { |
503 | /* 20->21, move views out of db */ |
504 | size_t size; |
505 | char *data; |
506 | |
507 | #define DB_TEXT_TYPE 'TEXT' |
508 | #define DB_TEXT_MENU_ID 1 |
509 | #define DB_TEXT_SHORTMENU_ID 2 |
510 | #define DB_TEXT_ISSUE_ID 3 |
511 | #define DB_TEXT_SIGNUP_ID 4 |
512 | #define DB_TEXT_PAGE_SYSOP_ID 5 |
513 | #define DB_TEXT_NO_FREE_NODES_ID 6 |
514 | #define DB_TEXT_SIGNOFF_ID 7 |
515 | #define DB_TEXT_MENU_OPTIONS_ID 8 |
516 | |
517 | progress("Migrating views out of database..."); |
518 | |
519 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
520 | DB_TEXT_MENU_ID, &data))) { |
521 | db_view_write(tdb, DB_VIEW_MENU, data, size); |
522 | xfree(&data); |
523 | bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_MENU_ID, 0); |
524 | } |
525 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
526 | DB_TEXT_SHORTMENU_ID, &data))) { |
527 | db_view_write(tdb, DB_VIEW_SHORTMENU, data, size); |
528 | xfree(&data); |
529 | bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SHORTMENU_ID, |
530 | 0); |
531 | } |
532 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
533 | DB_TEXT_ISSUE_ID, &data))) { |
534 | db_view_write(tdb, DB_VIEW_ISSUE, data, size); |
535 | xfree(&data); |
536 | bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_ISSUE_ID, 0); |
537 | } |
538 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
539 | DB_TEXT_SIGNUP_ID, &data))) { |
540 | db_view_write(tdb, DB_VIEW_SIGNUP, data, size); |
541 | xfree(&data); |
542 | bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SIGNUP_ID, 0); |
543 | } |
544 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
545 | DB_TEXT_PAGE_SYSOP_ID, &data))) { |
546 | db_view_write(tdb, DB_VIEW_PAGE_SYSOP, data, size); |
547 | xfree(&data); |
548 | bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_PAGE_SYSOP_ID, |
549 | 0); |
550 | } |
551 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
552 | DB_TEXT_NO_FREE_NODES_ID, &data))) { |
553 | db_view_write(tdb, DB_VIEW_NO_FREE_NODES, data, size); |
554 | xfree(&data); |
555 | bile_delete(tdb->bile, DB_TEXT_TYPE, |
556 | DB_TEXT_NO_FREE_NODES_ID, 0); |
557 | } |
558 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
559 | DB_TEXT_SIGNOFF_ID, &data))) { |
560 | db_view_write(tdb, DB_VIEW_SIGNOFF, data, size); |
561 | xfree(&data); |
562 | bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SIGNOFF_ID, 0); |
563 | } |
564 | if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, |
565 | DB_TEXT_MENU_OPTIONS_ID, &data))) { |
566 | db_view_write(tdb, DB_VIEW_MENU_OPTIONS, data, size); |
567 | xfree(&data); |
568 | bile_delete(tdb->bile, DB_TEXT_TYPE, |
569 | DB_TEXT_MENU_OPTIONS_ID, 0); |
570 | } |
571 | |
572 | break; |
573 | } |
574 | case 21: { |
575 | /* 20->21, syslog ip */ |
576 | struct config new_config = { 0 }; |
577 | |
578 | bile_read(tdb->bile, DB_CONFIG_RTYPE, 1, (char *)&new_config, |
579 | sizeof(new_config)); |
580 | |
581 | new_config.syslog_ip = 0; |
582 | |
583 | bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &new_config, |
584 | sizeof(new_config)); |
585 | break; |
586 | } |
587 | } |
588 | |
589 | ver++; |
590 | } |
591 | |
592 | progress(NULL); |
593 | |
594 | /* store new version */ |
595 | ver = DB_CUR_VERS; |
596 | if (bile_write(tdb->bile, DB_VERS_RTYPE, 1, &ver, 1) != 1) |
597 | panic("Failed writing new version: %d", bile_error(tdb->bile)); |
598 | |
599 | return 0; |
600 | } |
601 | |
602 | void |
603 | db_config_save(struct db *tdb) |
604 | { |
605 | if (bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &tdb->config, |
606 | sizeof(tdb->config)) != sizeof(tdb->config)) |
607 | panic("db_config_save: failed to write: %d", |
608 | bile_error(tdb->bile)); |
609 | } |
610 | |
611 | void |
612 | db_config_load(struct db *tdb) |
613 | { |
614 | size_t rlen; |
615 | char *newconfig; |
616 | |
617 | rlen = bile_read_alloc(tdb->bile, DB_CONFIG_RTYPE, 1, &newconfig); |
618 | if (rlen == 0 || bile_error(tdb->bile)) |
619 | panic("db_config_load: error reading config: %d", |
620 | bile_error(tdb->bile)); |
621 | if (rlen != sizeof(tdb->config)) |
622 | warn("db_config_load: read config of size %lu, but expected %lu", |
623 | rlen, sizeof(tdb->config)); |
624 | |
625 | memcpy(&tdb->config, newconfig, sizeof(tdb->config)); |
626 | xfree(&newconfig); |
627 | } |
628 | |
629 | void |
630 | db_cache_boards(struct db *tdb) |
631 | { |
632 | Str255 db_filename, board_filename; |
633 | struct board_id_time_map first_map; |
634 | size_t n, size; |
635 | unsigned long *ids; |
636 | struct bile_object *obj; |
637 | char *data = NULL; |
638 | |
639 | if (tdb->boards) { |
640 | for (n = 0; n < tdb->nboards; n++) { |
641 | if (tdb->boards[n].bile) |
642 | bile_close(tdb->boards[n].bile); |
643 | } |
644 | xfree(&tdb->boards); |
645 | } |
646 | |
647 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, |
648 | false) != 0) |
649 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
650 | PtoCstr(db_filename); |
651 | |
652 | tdb->nboards = bile_sorted_ids_by_type(tdb->bile, DB_BOARD_RTYPE, &ids); |
653 | if (!tdb->nboards) |
654 | return; |
655 | tdb->boards = xcalloc(tdb->nboards, sizeof(struct board)); |
656 | if (tdb->boards == NULL) { |
657 | tdb->nboards = 0; |
658 | return; |
659 | } |
660 | |
661 | /* |
662 | * Read ids first so we have a consistent order, then try to open or |
663 | * fix/recreate each bile, which may change their order in the map |
664 | */ |
665 | for (n = 0; n < tdb->nboards; n++) { |
666 | obj = bile_find(tdb->bile, DB_BOARD_RTYPE, ids[n]); |
667 | if (obj == NULL) |
668 | break; |
669 | |
670 | size = bile_read_alloc(tdb->bile, DB_BOARD_RTYPE, obj->id, &data); |
671 | bile_unmarshall_object(tdb->bile, board_object_fields, |
672 | nboard_object_fields, data, size, (char *)(&tdb->boards[n]), |
673 | sizeof(struct board), true); |
674 | xfree(&data); |
675 | xfree(&obj); |
676 | } |
677 | |
678 | xfree(&ids); |
679 | |
680 | for (n = 0; n < tdb->nboards; n++) { |
681 | snprintf((char *)board_filename, sizeof(board_filename), |
682 | "%s:%lu.%s", db_filename, tdb->boards[n].id, BOARD_FILENAME_EXT); |
683 | CtoPstr(board_filename); |
684 | tdb->boards[n].bile = bile_open(board_filename, |
685 | tdb->bile->vrefnum); |
686 | if (tdb->boards[n].bile != NULL) |
687 | goto opened; |
688 | |
689 | PtoCstr(board_filename); |
690 | |
691 | if (bile_error(NULL) != fnfErr && |
692 | ask("Attempt recovery of %s with backup map?", board_filename)) { |
693 | CtoPstr(board_filename); |
694 | tdb->boards[n].bile = bile_open_recover_map(board_filename, |
695 | tdb->bile->vrefnum); |
696 | if (tdb->boards[n].bile != NULL) |
697 | continue; |
698 | warn("Failed to recover board file %s: %d", |
699 | PtoCstr(board_filename), bile_error(NULL)); |
700 | } |
701 | |
702 | if (ask("Failed to open board bile %s (error %d), recreate it?", |
703 | board_filename, bile_error(NULL)) == false) |
704 | panic("Can't open board file, exiting"); |
705 | |
706 | tdb->boards[n].bile = db_board_create(tdb, &tdb->boards[n], true); |
707 | if (tdb->boards[n].bile == NULL) |
708 | panic("Failed to create board file %s: %d", |
709 | board_filename, bile_error(NULL)); |
710 | |
711 | opened: |
712 | size = bile_read(tdb->boards[n].bile, |
713 | BOARD_SORTED_ID_MAP_RTYPE, 1, &first_map, sizeof(first_map)); |
714 | if (size != sizeof(first_map)) { |
715 | logger_printf("[db] Reindexing ids on board %s", |
716 | tdb->boards[n].name); |
717 | board_index_sorted_post_ids(&tdb->boards[n], NULL); |
718 | size = bile_read(tdb->boards[n].bile, |
719 | BOARD_SORTED_ID_MAP_RTYPE, 1, &first_map, sizeof(first_map)); |
720 | if (size != sizeof(first_map)) { |
721 | tdb->boards[n].last_post_at = 0; |
722 | continue; |
723 | } |
724 | } |
725 | tdb->boards[n].last_post_at = first_map.time; |
726 | } |
727 | } |
728 | |
729 | struct bile * |
730 | db_board_create(struct db *tdb, struct board *board, bool delete_first) |
731 | { |
732 | Str255 db_filename, board_filename; |
733 | struct bile *board_bile; |
734 | size_t size; |
735 | short ret; |
736 | char *data; |
737 | |
738 | ret = bile_marshall_object(tdb->bile, board_object_fields, |
739 | nboard_object_fields, board, &data, &size); |
740 | if (ret != 0 || size == 0) { |
741 | warn("db_board_create: failed to marshall object"); |
742 | return NULL; |
743 | } |
744 | |
745 | if (bile_write(tdb->bile, DB_BOARD_RTYPE, board->id, data, |
746 | size) != size) |
747 | panic("save of new board failed: %d", bile_error(tdb->bile)); |
748 | xfree(&data); |
749 | |
750 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, |
751 | false) != 0) |
752 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
753 | PtoCstr(db_filename); |
754 | |
755 | snprintf((char *)&board_filename, sizeof(board_filename), "%s:%lu.%s", |
756 | db_filename, board->id, BOARD_FILENAME_EXT); |
757 | CtoPstr(board_filename); |
758 | |
759 | if (delete_first) |
760 | FSDelete(board_filename, tdb->bile->vrefnum); |
761 | |
762 | board_bile = bile_create(board_filename, tdb->bile->vrefnum, |
763 | SUBTEXT_CREATOR, DB_BOARD_RTYPE); |
764 | if (board_bile == NULL) |
765 | panic("Failed creating new board bile at %s: %d", |
766 | PtoCstr(board_filename), bile_error(NULL)); |
767 | |
768 | return board_bile; |
769 | } |
770 | |
771 | void |
772 | db_board_delete(struct db *tdb, struct board *board) |
773 | { |
774 | if (bile_delete(tdb->bile, DB_BOARD_RTYPE, board->id, 0) != 0) { |
775 | warn("deletion of board %ld failed: %d", board->id, |
776 | bile_error(tdb->bile)); |
777 | return; |
778 | } |
779 | bile_close(board->bile); |
780 | } |
781 | |
782 | void |
783 | db_cache_folders(struct db *tdb) |
784 | { |
785 | Str255 db_filename, folder_filename, folder_dir; |
786 | size_t n, size; |
787 | unsigned long *ids, id; |
788 | struct bile_object *obj; |
789 | char *data = NULL; |
790 | short error; |
791 | |
792 | if (tdb->folders) { |
793 | for (n = 0; n < tdb->nfolders; n++) { |
794 | if (tdb->folders[n].bile) |
795 | bile_close(tdb->folders[n].bile); |
796 | } |
797 | xfree(&tdb->folders); |
798 | } |
799 | |
800 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, |
801 | false) != 0) |
802 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
803 | PtoCstr(db_filename); |
804 | |
805 | tdb->nfolders = bile_sorted_ids_by_type(tdb->bile, DB_FOLDER_RTYPE, |
806 | &ids); |
807 | if (!tdb->nfolders) |
808 | return; |
809 | tdb->folders = xcalloc(tdb->nfolders, sizeof(struct folder)); |
810 | if (tdb->folders == NULL) { |
811 | tdb->nfolders = 0; |
812 | return; |
813 | } |
814 | |
815 | /* |
816 | * Read ids first so we have a consistent order, then try to open or |
817 | * fix/recreate each bile, which may change their order in the map |
818 | */ |
819 | for (n = 0; n < tdb->nfolders; n++) { |
820 | obj = bile_find(tdb->bile, DB_FOLDER_RTYPE, ids[n]); |
821 | if (obj == NULL) |
822 | break; |
823 | |
824 | size = bile_read_alloc(tdb->bile, DB_FOLDER_RTYPE, obj->id, &data); |
825 | if (size == 0 || data == NULL) |
826 | break; |
827 | bile_unmarshall_object(tdb->bile, folder_object_fields, |
828 | nfolder_object_fields, data, size, (char *)(&tdb->folders[n]), |
829 | sizeof(struct folder), true); |
830 | xfree(&data); |
831 | } |
832 | |
833 | xfree(&ids); |
834 | |
835 | for (n = 0; n < tdb->nfolders; n++) { |
836 | snprintf((char *)folder_filename, sizeof(folder_filename), |
837 | "%s:%lu.%s", db_filename, tdb->folders[n].id, |
838 | FOLDER_FILENAME_EXT); |
839 | CtoPstr(folder_filename); |
840 | tdb->folders[n].bile = bile_open(folder_filename, |
841 | tdb->bile->vrefnum); |
842 | if (tdb->folders[n].bile != NULL) |
843 | continue; |
844 | |
845 | PtoCstr(folder_filename); |
846 | |
847 | if (bile_error(NULL) != fnfErr && |
848 | ask("Attempt recovery of %s with backup map?", folder_filename)) { |
849 | CtoPstr(folder_filename); |
850 | tdb->folders[n].bile = bile_open_recover_map(folder_filename, |
851 | tdb->bile->vrefnum); |
852 | if (tdb->folders[n].bile != NULL) |
853 | continue; |
854 | warn("Failed to recover folder bile %s: %d", |
855 | PtoCstr(folder_filename), bile_error(NULL)); |
856 | } |
857 | |
858 | if (ask("Failed to open folder bile %s (error %d), recreate it?", |
859 | folder_filename, bile_error(NULL)) == false) |
860 | exit(0); |
861 | |
862 | tdb->folders[n].bile = db_folder_create(tdb, &tdb->folders[n], |
863 | true); |
864 | if (tdb->folders[n].bile == NULL) |
865 | panic("Failed to create folder bile %s: %d", |
866 | folder_filename, bile_error(NULL)); |
867 | } |
868 | |
869 | for (n = 0; n < tdb->nfolders; n++) { |
870 | /* make sure directory exists */ |
871 | memcpy(folder_dir, tdb->folders[n].path, sizeof(folder_dir)); |
872 | CtoPstr(folder_dir); |
873 | if (!FIsDir(folder_dir) && |
874 | ask("Folder %ld path \"%s\" does not exist, create it?", |
875 | tdb->folders[n].id, tdb->folders[n].path)) { |
876 | error = DirCreate(db->bile->vrefnum, 0, folder_dir, &id); |
877 | if (error) |
878 | warn("Failed creating %s: %d", tdb->folders[n].path, |
879 | error); |
880 | } |
881 | } |
882 | } |
883 | |
884 | struct bile * |
885 | db_folder_create(struct db *tdb, struct folder *folder, bool delete_first) |
886 | { |
887 | Str255 db_filename, folder_filename, folder_dir; |
888 | struct bile *folder_bile; |
889 | size_t size; |
890 | short ret, newid, error; |
891 | char *data; |
892 | |
893 | ret = bile_marshall_object(tdb->bile, folder_object_fields, |
894 | nfolder_object_fields, folder, &data, &size); |
895 | if (ret != 0 || size == 0) { |
896 | warn("db_folder_create: failed to marshall object"); |
897 | return NULL; |
898 | } |
899 | |
900 | if (bile_write(tdb->bile, DB_FOLDER_RTYPE, folder->id, data, |
901 | size) != size) |
902 | panic("save of new folder failed: %d", bile_error(tdb->bile)); |
903 | xfree(&data); |
904 | |
905 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, |
906 | false) != 0) |
907 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
908 | PtoCstr(db_filename); |
909 | |
910 | snprintf((char *)&folder_filename, sizeof(folder_filename), |
911 | "%s:%lu.%s", db_filename, folder->id, FOLDER_FILENAME_EXT); |
912 | CtoPstr(folder_filename); |
913 | |
914 | if (delete_first) |
915 | FSDelete(folder_filename, tdb->bile->vrefnum); |
916 | |
917 | folder_bile = bile_create(folder_filename, tdb->bile->vrefnum, |
918 | SUBTEXT_CREATOR, DB_FOLDER_RTYPE); |
919 | if (folder_bile == NULL) |
920 | panic("Failed creating new folder bile at %s: %d", |
921 | PtoCstr(folder_filename), bile_error(NULL)); |
922 | |
923 | memcpy(&folder_dir, folder->path, sizeof(folder_dir)); |
924 | CtoPstr(folder_dir); |
925 | if (!FIsDir(folder_dir)) { |
926 | error = DirCreate(tdb->bile->vrefnum, 0, folder_dir, &newid); |
927 | if (error) |
928 | warn("Failed creating %s: %d", folder->path, error); |
929 | } |
930 | |
931 | return folder_bile; |
932 | } |
933 | |
934 | void |
935 | db_folder_delete(struct db *tdb, struct folder *folder) |
936 | { |
937 | if (bile_delete(tdb->bile, DB_FOLDER_RTYPE, folder->id, 0) != 0) { |
938 | warn("deletion of folder %ld failed: %d", folder->id, |
939 | bile_error(tdb->bile)); |
940 | return; |
941 | } |
942 | bile_close(folder->bile); |
943 | } |
944 | |
945 | const char * |
946 | db_view_filename(short id) |
947 | { |
948 | switch (id) { |
949 | case DB_VIEW_MENU: |
950 | return "menu.txt"; |
951 | case DB_VIEW_SHORTMENU: |
952 | return "short_menu.txt"; |
953 | case DB_VIEW_ISSUE: |
954 | return "issue.txt"; |
955 | case DB_VIEW_SIGNUP: |
956 | return "signup.txt"; |
957 | case DB_VIEW_PAGE_SYSOP: |
958 | return "page_sysop.txt"; |
959 | case DB_VIEW_NO_FREE_NODES: |
960 | return "no_free_nodes.txt"; |
961 | case DB_VIEW_SIGNOFF: |
962 | return "signoff.txt"; |
963 | case DB_VIEW_MENU_OPTIONS: |
964 | return "menu_options.txt"; |
965 | default: |
966 | panic("db_view_filename: unknown id %d", id); |
967 | } |
968 | } |
969 | |
970 | void |
971 | db_cache_views(struct db *tdb) |
972 | { |
973 | Str255 viewdir, viewpath; |
974 | struct stat sb; |
975 | struct main_menu_option *opts; |
976 | Handle default_menu; |
977 | short n; |
978 | size_t size; |
979 | short error, frefnum; |
980 | |
981 | logger_printf("[db] Caching views"); |
982 | |
983 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, viewdir, |
984 | false) != 0) |
985 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
986 | PtoCstr(viewdir); |
987 | strlcat((char *)viewdir, ":views:", sizeof(Str255)); |
988 | |
989 | for (n = 0; n < DB_VIEW_COUNT; n++) { |
990 | if (tdb->views[n]) |
991 | xfree(&tdb->views[n]); |
992 | tdb->views[n] = NULL; |
993 | |
994 | strlcpy((char *)viewpath, (char *)viewdir, sizeof(Str255)); |
995 | strlcat((char *)viewpath, db_view_filename(n), sizeof(Str255)); |
996 | CtoPstr(viewpath); |
997 | |
998 | if (FStat(viewpath, &sb) != 0) { |
999 | if (n == DB_VIEW_MENU_OPTIONS) { |
1000 | default_menu = GetResource('TEXT', MENU_DEFAULTS_ID); |
1001 | if (default_menu == NULL) |
1002 | panic("Failed to find TEXT resource %d for menu options", |
1003 | MENU_DEFAULTS_ID); |
1004 | size = GetHandleSize(default_menu); |
1005 | HLock(default_menu); |
1006 | db_view_write(tdb, n, *default_menu, size); |
1007 | HUnlock(default_menu); |
1008 | ReleaseResource(default_menu); |
1009 | FStat(viewpath, &sb); |
1010 | } else { |
1011 | /* create a blank file so the sysop knows what to edit */ |
1012 | db_view_write(tdb, n, "", 0); |
1013 | |
1014 | /* but leave the view NULL */ |
1015 | continue; |
1016 | } |
1017 | } |
1018 | |
1019 | if (sb.st_size == 0) |
1020 | continue; |
1021 | |
1022 | tdb->views[n] = xmalloc(sb.st_size + 1); |
1023 | if (tdb->views[n] == NULL) |
1024 | panic("Failed allocating %ld for view %s", |
1025 | sb.st_size, PtoCstr(viewpath)); |
1026 | |
1027 | error = FSOpen(viewpath, 0, &frefnum); |
1028 | if (error) |
1029 | panic("Error opening view %s: %d", PtoCstr(viewpath), error); |
1030 | |
1031 | size = sb.st_size; |
1032 | error = FSRead(frefnum, &size, tdb->views[n]); |
1033 | FSClose(frefnum); |
1034 | |
1035 | if (error && error != eofErr) |
1036 | panic("Error reading view %s: %d", PtoCstr(viewpath), |
1037 | error); |
1038 | |
1039 | if (size > sb.st_size) |
1040 | panic("FSRead read more (%ld) than file size (%ld) for %s", |
1041 | size, sb.st_size, PtoCstr(viewpath)); |
1042 | |
1043 | tdb->views[n][size] = '\0'; |
1044 | |
1045 | if (n == DB_VIEW_MENU_OPTIONS) { |
1046 | opts = main_menu_parse(tdb->views[n], sb.st_size); |
1047 | if (opts) { |
1048 | if (main_menu_options != NULL) |
1049 | xfree(&main_menu_options); |
1050 | main_menu_options = opts; |
1051 | } |
1052 | } |
1053 | } |
1054 | } |
1055 | |
1056 | void |
1057 | db_view_write(struct db *tdb, short id, char *str, size_t size) |
1058 | { |
1059 | Str255 viewpath; |
1060 | size_t len; |
1061 | short error, frefnum; |
1062 | |
1063 | if (getpath(tdb->bile->vrefnum, tdb->bile->filename, viewpath, |
1064 | false) != 0) |
1065 | panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); |
1066 | PtoCstr(viewpath); |
1067 | strlcat((char *)viewpath, ":views:", sizeof(Str255)); |
1068 | strlcat((char *)viewpath, db_view_filename(id), sizeof(Str255)); |
1069 | logger_printf("[db] Writing default view %s", viewpath); |
1070 | CtoPstr(viewpath); |
1071 | |
1072 | error = Create(viewpath, 0, 'ttxt', 'TEXT'); |
1073 | if (error == dupFNErr) |
1074 | panic("View file already exists: %s", PtoCstr(viewpath)); |
1075 | if (error) |
1076 | panic("Failed creating %s: %d", PtoCstr(viewpath), error); |
1077 | |
1078 | if ((error = FSOpen(viewpath, 0, &frefnum))) |
1079 | panic("Failed opening newly-created %s: %d", PtoCstr(viewpath), |
1080 | error); |
1081 | |
1082 | len = size; |
1083 | if ((error = Allocate(frefnum, &len))) |
1084 | panic("Failed setting %s to size %ld: %d", PtoCstr(viewpath), |
1085 | size, error); |
1086 | |
1087 | len = size; |
1088 | if ((error = FSWrite(frefnum, &len, str))) |
1089 | panic("Failed writing view to file %s: %d", PtoCstr(viewpath), |
1090 | error); |
1091 | if (len != size) |
1092 | panic("Short write of view to file %s: %ld != %ld", |
1093 | PtoCstr(viewpath), len, size); |
1094 | |
1095 | FSClose(frefnum); |
1096 | } |