diff options
author | piaip <piaip@63ad8ddf-47c3-0310-b6dd-a9e9d9715204> | 2010-10-29 22:25:48 +0800 |
---|---|---|
committer | piaip <piaip@63ad8ddf-47c3-0310-b6dd-a9e9d9715204> | 2010-10-29 22:25:48 +0800 |
commit | fccf503a0804f0258bc2cda65921c2cb45bb2e4a (patch) | |
tree | fbb1bd3c7ad67cc89b6366324bd72ffede63a39d | |
parent | 1a149a5b96b90fb904b2d34857e43e11f7ec108f (diff) | |
download | pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.tar pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.tar.gz pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.tar.bz2 pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.tar.lz pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.tar.xz pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.tar.zst pttbbs-fccf503a0804f0258bc2cda65921c2cb45bb2e4a.zip |
New system: Time Capsule (Magical Index)
Successor of edit history, powered by Panty&Stocking Browser,
the Time Capsule provides integrated interface for manipulating
deleted objects (recycle bin) and tracking modification history.
git-svn-id: http://opensvn.csie.org/pttbbs/trunk@5179 63ad8ddf-47c3-0310-b6dd-a9e9d9715204
-rw-r--r-- | pttbbs/include/common.h | 5 | ||||
-rw-r--r-- | pttbbs/include/config.h | 8 | ||||
-rw-r--r-- | pttbbs/include/modes.h | 1 | ||||
-rw-r--r-- | pttbbs/include/proto.h | 19 | ||||
-rw-r--r-- | pttbbs/mbbsd/Makefile | 2 | ||||
-rw-r--r-- | pttbbs/mbbsd/bbs.c | 220 | ||||
-rw-r--r-- | pttbbs/mbbsd/mail.c | 30 | ||||
-rw-r--r-- | pttbbs/mbbsd/psb.c | 280 | ||||
-rw-r--r-- | pttbbs/mbbsd/read.c | 3 | ||||
-rw-r--r-- | pttbbs/mbbsd/timecap.c | 339 |
10 files changed, 754 insertions, 153 deletions
diff --git a/pttbbs/include/common.h b/pttbbs/include/common.h index 7609d1d0..fa614eba 100644 --- a/pttbbs/include/common.h +++ b/pttbbs/include/common.h @@ -53,7 +53,6 @@ #define SZ_RECENTPAY (16000) #endif - // 自訂刪除文章時出現的標題與檔案 #ifndef FN_SAFEDEL #ifdef USE_EDIT_HISTORY @@ -69,6 +68,10 @@ #endif #define FN_EDITHISTORY ".history" +#ifndef SAFE_ARTICLE_DELETE_NUSER +#define SAFE_ARTICLE_DELETE_NUSER (2) +#endif + #define MSG_DEL_CANCEL "取消刪除" #define MSG_BIG_BOY "我是大帥哥! ^o^Y" #define MSG_BIG_GIRL "世紀大美女 *^-^*" diff --git a/pttbbs/include/config.h b/pttbbs/include/config.h index 401a8d11..b74f6e2a 100644 --- a/pttbbs/include/config.h +++ b/pttbbs/include/config.h @@ -111,6 +111,14 @@ #define BN_UNANONYMOUS "UnAnonymous" #endif +#ifndef RECYCLE_BIN_NAME +#define RECYCLE_BIN_NAME "資源回收筒" // "垃圾桶" +#endif + +#ifndef TIME_CAPSULE_NAME +#define TIME_CAPSULE_NAME "Magical Index" // "Time Capsule" +#endif + /* Environment */ #ifndef RELAY_SERVER_IP /* 寄站外信的 mail server */ #define RELAY_SERVER_IP "127.0.0.1" diff --git a/pttbbs/include/modes.h b/pttbbs/include/modes.h index 21485f9b..c35d6ae1 100644 --- a/pttbbs/include/modes.h +++ b/pttbbs/include/modes.h @@ -28,6 +28,7 @@ #define RET_SELECTBRD (992) #define RET_DOREPLYALL (991) #define RET_SELECTAID (990) +#define RET_RECYCLEBIN (989) /* user 操作狀態與模式 */ #define IDLE 0 diff --git a/pttbbs/include/proto.h b/pttbbs/include/proto.h index bb0913c8..3502fb91 100644 --- a/pttbbs/include/proto.h +++ b/pttbbs/include/proto.h @@ -159,7 +159,9 @@ int ccw_talk(int fd, int destuid); // common chat window: private talk int ccw_chat(int fd); // common chat window: chatroom /* psb (panty and stocking browser) */ -int psb_view_edit_history(const char *base, const char *subject, int max_hist); +int psb_view_edit_history(const char *base, const char *subject, + int maxrev, int current_as_base); +int psb_recycle_bin(const char *base, const char *title); int psb_admin_edit(); /* chc */ @@ -650,6 +652,21 @@ int term_init(void); void term_resize(int w, int h); void bell(void); +/* timecap (time capsule) */ +int timecapsule_add_revision(const char *filename); +int timecapsule_get_max_revision_number(const char *filename); +int timecapsule_get_max_archive_number(const char *filename, size_t szrefblob); +int timecapsule_archive(const char *filename, const void *ref, size_t szref); +int timecapsule_get_by_revision(const char *filename, int rev, char *rev_path, + size_t sz_rev_path); +int timecapsule_archive_new_revision(const char *filename, + const void *ref, size_t szref, + char *archived_path, size_t sz_archived_path); +int timecapsule_get_archive_by_index(const char *filename, int idx, + void *refblob, size_t szrefblob); +int timecapsule_get_archive_blobs(const char *filename, int idx, int nblobs, + void *blobsptr, size_t szblob); + /* ordersong */ int ordersong(void); int topsong(void); diff --git a/pttbbs/mbbsd/Makefile b/pttbbs/mbbsd/Makefile index 31f0d71a..190bc114 100644 --- a/pttbbs/mbbsd/Makefile +++ b/pttbbs/mbbsd/Makefile @@ -15,7 +15,7 @@ TALKOBJS = friend.o talk.o ccw.o UTILOBJS = stuff.o kaede.o convert.o name.o syspost.o cache.o cal.o UIOBJS = menu.o vtuikit.o psb.o PAGEROBJS= more.o pmore.o -PLUGOBJS = calendar.o ordersong.o gamble.o angel.o +PLUGOBJS = calendar.o ordersong.o gamble.o angel.o timecap.o CHESSOBJS= chess.o chc.o chc_tab.o ch_go.o ch_gomo.o ch_dark.o ch_reversi.o GAMEOBJS = card.o chicken.o OBJS:= admin.o assess.o edit.o xyz.o var.o vote.o voteboard.o \ diff --git a/pttbbs/mbbsd/bbs.c b/pttbbs/mbbsd/bbs.c index 2bb60392..4b49a2fc 100644 --- a/pttbbs/mbbsd/bbs.c +++ b/pttbbs/mbbsd/bbs.c @@ -136,49 +136,6 @@ modify_dir_lite( return 0; } -#ifdef USE_EDIT_HISTORY -static int -add_to_post_history( - const char *direct, const char *basename, - const char *old_path, const char *new_path) -{ - char hist_file[PATHLEN]; - char hist_num[STRLEN]; - int fd = 0, last_index = 0; - - setdirpath(hist_file, direct, FN_EDITHISTORY "/"); - if (!dashd(hist_file)) - Mkdir(hist_file); - strlcat(hist_file, basename, sizeof(hist_file)); - - if ((fd = OpenCreate(hist_file, O_RDWR)) >= 0) { - // XXX if somebody just die inside... locks everyone! - flock(fd, LOCK_EX); - read(fd, &last_index, sizeof(last_index)); - last_index++; - lseek(fd, 0, SEEK_SET); - write(fd, &last_index, sizeof(last_index)); - flock(fd, LOCK_UN); - close(fd); - - if (last_index == 1) { - char * const p = hist_file + strlen(hist_file); - // rev 0->1, let's make a copy of original version - strlcat(hist_file, ".000", sizeof(hist_file)); - HardLink(old_path, hist_file); - *p = 0; - } - - // now build the history file - sprintf(hist_num, ".%03d", last_index); - strlcat(hist_file, hist_num, sizeof(hist_file)); - HardLink(new_path, hist_file); - return 0; - } - return -1; -} -#endif - static void check_locked(fileheader_t *fhdr) { @@ -840,6 +797,67 @@ outgo_post(const fileheader_t *fh, const char *board, const char *userid, const } } +#ifdef USE_TIME_CAPSULE + +static void +innd_cancel_post(const fileheader_t *fh, const char *fpath, const char *userid) +{ + // deal with innd + FILE *fp = fopen(fpath, "r"); + char buf[STRLEN*2]; + char *s, *e, *nick = ""; + + // searching one line is enough. + if (fp && + fgets(buf, sizeof(buf), fp) && + (strncmp(buf, str_author1, LEN_AUTHOR1) == 0 || + strncmp(buf, str_author2, LEN_AUTHOR2) == 0) && + (s = strchr(buf, '(')) && + (e = strrchr(buf, ')'))) { + *s ++ = 0; + *e = 0; + nick = s; + } + if (fp) { + fclose(fp); + // record to NNTP + log_filef("innd/cancel.bntp", LOG_CREAT, + "%s\t%s\t%s\t%s\t%s\n", + currboard, fh->filename, userid, nick, fh->title); + } +} + +static int +cancelpost2(const fileheader_t *fh, char *newpath, size_t sznewpath) { + char fpath[PATHLEN]; + int ret = 0; + + if(!fh->filename[0]) + return -1; + + setbfile(fpath, currboard, fh->filename); + // if (!dashf(fpath)) return -1; + log_filef(fpath, LOG_CREAT, "\n※ Deleted by: %s (%s) %s", + cuser.userid, fromhost, Cdatelite(&now)); + + if (!timecapsule_archive_new_revision( + fpath, fh, sizeof(*fh), newpath, sznewpath)) + ret = -1; + + // the file should be already in time capsule + if (unlink(fpath) != 0) + ret = -1; + + // should we use cuser.userid, or userid in post? + // I don't know, simply following the old way in cancelpost... + if (!(currbrdattr & BRD_NOTRAN)) + innd_cancel_post(fh, fpath, cuser.userid); + + return ret; +} + +#else + static int cancelpost(const fileheader_t *fh, int by_BM, char *newpath) { @@ -891,6 +909,8 @@ cancelpost(const fileheader_t *fh, int by_BM, char *newpath) return ret; } +#endif + static void do_deleteCrossPost(const fileheader_t *fh, char bname[]) { @@ -916,7 +936,8 @@ do_deleteCrossPost(const fileheader_t *fh, char bname[]) if( (i=getindex(bdir, &newfh, 0))>0) { #ifdef SAFE_ARTICLE_DELETE - if(bp && !(currmode & MODE_DIGEST) && bp->nuser > 30 ) + if(bp && !(currmode & MODE_DIGEST) && + bp->nuser >= SAFE_ARTICLE_DELETE_NUSER) safe_article_delete(i, &newfh, bdir, NULL); else #endif @@ -1785,11 +1806,9 @@ edit_post(int ent, fileheader_t * fhdr, const char *direct) } // OK to save file. - -#ifdef USE_EDIT_HISTORY - add_to_post_history(direct, fhdr->filename, genbuf, fpath); +#ifdef USE_TIME_CAPSULE + timecapsule_add_revision(genbuf); #endif - // piaip Wed Jan 9 11:11:33 CST 2008 // in order to prevent calling system 'mv' all the // time, it is better to unlink() first, which @@ -3116,7 +3135,8 @@ del_range(int ent, const fileheader_t *fhdr, const char *direct) outmsg("處理中,請稍後..."); refresh(); #ifdef SAFE_ARTICLE_DELETE - if(bp && !(currmode & MODE_DIGEST) && bp->nuser > 30) + if(bp && !(currmode & MODE_DIGEST) && + bp->nuser >= SAFE_ARTICLE_DELETE_NUSER) ret = safe_article_delete_range(direct, inum1, inum2); else #endif @@ -3149,7 +3169,7 @@ static int del_post(int ent, fileheader_t * fhdr, char *direct) { char reason[PROPER_TITLE_LEN] = ""; - char genbuf[100], newpath[PATHLEN]; + char genbuf[100], newpath[PATHLEN] = ""; int not_owned, is_anon, tusernum, del_ok = 0, as_badpost = 0; boardheader_t *bp; @@ -3274,7 +3294,8 @@ del_post(int ent, fileheader_t * fhdr, char *direct) if( #ifdef SAFE_ARTICLE_DELETE - ((reason[0] || bp->nuser > 30) && !(currmode & MODE_DIGEST) && + ((reason[0] || bp->nuser >= SAFE_ARTICLE_DELETE_NUSER) && + !(currmode & MODE_DIGEST) && !safe_article_delete(ent, fhdr, direct, reason[0] ? reason : NULL)) || #endif // XXX TODO delete_record is really really dangerous - @@ -3286,7 +3307,11 @@ del_post(int ent, fileheader_t * fhdr, char *direct) // was closed. setbtotal(currbid); +#ifdef USE_TIME_CAPSULE + del_ok = (cancelpost2(fhdr, newpath, sizeof(newpath)) == 0) ? 1 : 0; +#else del_ok = (cancelpost(fhdr, not_owned, newpath) == 0) ? 1 : 0; +#endif deleteCrossPost(fhdr, bp->brdname); #ifdef ASSESS @@ -3298,10 +3323,10 @@ del_post(int ent, fileheader_t * fhdr, char *direct) // do nothing } // case 2, got error in file deletion (already deleted, also skip badpost) - else if (!del_ok) + else if (!del_ok || !*newpath) { move_ansi(1, 40); clrtoeol(); - outs("此檔已被別人刪除(跳過劣文設定)"); + outs("已刪或刪除錯誤(跳過劣文設定)"); pressanykey(); } // case 3, post older than one week (TODO use macro for the duration) @@ -3369,6 +3394,9 @@ del_post(int ent, fileheader_t * fhdr, char *direct) cuser.numposts, del_fee); } + if (!del_ok) + vmsg("刪除過程發生錯誤,請向" BN_BUGREPORT "報告"); + return DIRCHANGED; } // delete_record } // genbuf[0] == 'y' @@ -3575,75 +3603,71 @@ view_postinfo(int ent, const fileheader_t * fhdr, const char *direct, int crs_ln return FULLUPDATE; } -#ifdef USE_EDIT_HISTORY +#ifdef USE_TIME_CAPSULE static int -view_post_history(int ent, const fileheader_t * fhdr, const char *direct) +view_posthistory(int ent, const fileheader_t * fhdr, const char *direct) { - const char *err_no_history = "抱歉,此篇文章暫無編輯歷史記錄可供檢視"; - char hist_file[PATHLEN]; - int fd, maxhist = 0; + char fpath[PATHLEN]; + const char *err_no_history = "此篇文章暫無編輯歷史記錄。" + "要進資源回收筒請再按一次 ~"; + int maxrev = 0; + int current_as_base = 1; // TODO allow author? if (!(currmode & MODE_BOARD)) return DONOTHING; - if ((!fhdr) || - ((fhdr->filename[0] == '.' || !fhdr->filename[0]) && -#ifdef FN_SAFEDEL_PREFIX_LEN - (strncmp(fhdr->filename, FN_SAFEDEL, FN_SAFEDEL_PREFIX_LEN) != 0) -#else - (strcmp(fhdr->filename, FN_SAFEDEL) != 0) -#endif - )) { - vmsg(err_no_history); + if (!fhdr || !fhdr->filename[0]) { + if (vmsg(err_no_history) == '~') + psb_recycle_bin(direct, currboard); return FULLUPDATE; } - // build history index file name - setdirpath(hist_file, direct, FN_EDITHISTORY "/"); - // XXX there are, well, unfortunately two kinds of deleted filename here: - // M.12345678.AAA -> - // - (old) .d<NUL>2345678.AAA # planned to be removed in the future - // - (new) .d12345678.AAA - if (strcmp(FN_SAFEDEL, fhdr->filename) == 0) { - assert(strlen(FN_SAFEDEL) == strlen("M.")); - // M.1 is a dirty hack, anyway.. - strlcat(hist_file, "M.1", sizeof(hist_file)); - strlcat(hist_file, fhdr->filename + strlen(FN_SAFEDEL) + 1, sizeof(hist_file)); + // assert(FN_SAFEDEL[0] == '.') + if ((fhdr->filename[0] == '.')) { + if ( #ifdef FN_SAFEDEL_PREFIX_LEN - } else if (strncmp(FN_SAFEDEL, fhdr->filename, FN_SAFEDEL_PREFIX_LEN) == 0) { - assert(FN_SAFEDEL_PREFIX_LEN == 2); // current pattern: 2 = M. - strlcat(hist_file, "M.", sizeof(hist_file)); - strlcat(hist_file, fhdr->filename + FN_SAFEDEL_PREFIX_LEN, sizeof(hist_file)); + (strncmp(fhdr->filename, FN_SAFEDEL, FN_SAFEDEL_PREFIX_LEN) != 0) +#else + (strcmp(fhdr->filename, FN_SAFEDEL) != 0) #endif - } else { - strlcat(hist_file, fhdr->filename, sizeof(hist_file)); + ) { + if (vmsg(err_no_history) == '~') + psb_recycle_bin(direct, currboard); + return FULLUPDATE; + } + current_as_base = 0; } - if (!dashf(hist_file) || - (fd = open(hist_file, O_RDONLY)) < 0) { - vmsg(err_no_history); - return FULLUPDATE; - } - read(fd, &maxhist, sizeof(maxhist)); - close(fd); - if (maxhist < 1) { - vmsg(err_no_history); + setbfile(fpath, currboard, fhdr->filename); + if (!current_as_base) { + char *prefix = strrchr(fpath, '/'); + assert(prefix); + // hard-coded file name conversion + *++prefix = 'M'; + *++prefix = '.'; + } + maxrev = timecapsule_get_max_revision_number(fpath); + if (maxrev < 1) { + if (vmsg(err_no_history) == '~') + psb_recycle_bin(direct, currboard); return FULLUPDATE; } - psb_view_edit_history(hist_file, fhdr->title, maxhist+1); + if (RET_RECYCLEBIN == + psb_view_edit_history(fpath, fhdr->title, maxrev, current_as_base)) + psb_recycle_bin(direct, currboard); return FULLUPDATE; } -#else // USE_EDIT_HISTORY +#else // USE_TIME_CAPSULE static int -view_post_history(int ent, const fileheader_t * fhdr, const char *direct) { +view_posthistory(int ent, const fileheader_t * fhdr, const char *direct) { return DONOTHING; } -#endif // USE_EDIT_HISTORY +#endif // USE_TIME_CAPSULE #ifdef OUTJOBSPOOL /* 看板備份 */ @@ -4156,7 +4180,7 @@ const onekey_t read_comms[] = { { 0, NULL }, // '{' 123 { 0, NULL }, // '|' 124 { 0, NULL }, // '}' 125 - { 1, view_post_history }, // '~' 126 + { 1, view_posthistory }, // '~' 126 }; int diff --git a/pttbbs/mbbsd/mail.c b/pttbbs/mbbsd/mail.c index 84c4d69c..4a0358ae 100644 --- a/pttbbs/mbbsd/mail.c +++ b/pttbbs/mbbsd/mail.c @@ -1108,8 +1108,9 @@ mailtitle(void) } showtitle("郵件選單", BBSName); - prints("[←]離開[↑↓]選擇[→]閱\讀信件 [O]站外信:%s [h]求助\n" , - REJECT_OUTTAMAIL(cuser) ? ANSI_COLOR(31) "關" ANSI_RESET : "開"); + prints("[←]離開[↑↓]選擇[→]閱\讀信件 [O]站外信:%s [h]求助 %s\n" , + REJECT_OUTTAMAIL(cuser) ? ANSI_COLOR(31) "關" ANSI_RESET : "開", + ANSI_COLOR(1;33) "[~]" RECYCLE_BIN_NAME "(新)" ANSI_RESET); vbarf(ANSI_REVERSE " 編號 %s 作 者 信 件 標 題\t%s ", (showmail_mode == SHOWMAIL_SUM) ? "大 小":"日 期", buf); @@ -1225,9 +1226,10 @@ mail_del(int ent, const fileheader_t * fhdr, const char *direct) if (!delete_record(direct, sizeof(*fhdr), ent)) { setupmailusage(); setdirpath(genbuf, direct, fhdr->filename); -#ifdef USE_RECYCLE - RcyAddFile(fhdr, 0, genbuf); -#endif // USE_RECYCLE +#ifdef USE_TIME_CAPSULE + timecapsule_archive_new_revision( + genbuf, fhdr, sizeof(*fhdr), NULL, 0); +#endif // USE_TIME_CAPSULE unlink(genbuf); loadmailusage(); return DIRCHANGED; @@ -1842,6 +1844,22 @@ mail_waterball(int ent GCC_UNUSED, fileheader_t * fhdr, const char *direct GCC_U return FULLUPDATE; } #endif + +#ifdef USE_TIME_CAPSULE +static int +mail_recycle_bin(int ent, fileheader_t * fhdr, const char *direct) { + psb_recycle_bin(direct, "個人信箱"); + return FULLUPDATE; +} +#else // USE_TIME_CAPSULE +static int +mail_recycle_bin(int ent GCC_UNUSED, + fileheader_t * fhdr GCC_UNUSED, + const char *direct GCC_UNUSED) { + return DONOTHING; +} +#endif // USE_TIME_CAPSULE + static const onekey_t mail_comms[] = { { 0, NULL }, // Ctrl('A') { 0, NULL }, // Ctrl('B') @@ -1937,7 +1955,7 @@ static const onekey_t mail_comms[] = { { 0, NULL }, // '{' 123 { 0, NULL }, // '|' 124 { 0, NULL }, // '}' 125 - { 0, NULL }, // '~' 126 + { 1, mail_recycle_bin }, // '~' 126 }; int diff --git a/pttbbs/mbbsd/psb.c b/pttbbs/mbbsd/psb.c index adff89b3..38bb1a44 100644 --- a/pttbbs/mbbsd/psb.c +++ b/pttbbs/mbbsd/psb.c @@ -63,8 +63,11 @@ psb_default_renderer(int i, int curr, int total, int rows, void *ctx) { static int psb_default_cursor(int y, int curr, void * ctx) { - outs("=>"); - // cursor_show(y, 0); +#ifdef USE_PFTERM + outs("●\b"); +#else + cursor_show(y, 0); +#endif return 0; } @@ -195,17 +198,23 @@ psb_main(PSB_CTX *psbctx) } /////////////////////////////////////////////////////////////////////////// -// View Edit History +// Time Capsule: Edit History + +#define PVEH_LIMIT_NUMBER (199) typedef struct { const char *subject; const char *filebase; + int leave_for_recycle_bin; + int rev_base; + int base_as_current; + time4_t *timestamps; } pveh_ctx; static int pveh_header(void *ctx) { pveh_ctx *cx = (pveh_ctx*) ctx; - vs_hdr2barf(" 【檢視文章編輯歷史】 \t %s", cx->subject); + vs_hdr2barf(" 【" TIME_CAPSULE_NAME ": 編輯歷史】 \t %s", cx->subject); move(1, 0); outs("請注意本系統不會永久保留所有的編輯歷史。"); outs("\n"); @@ -215,20 +224,20 @@ pveh_header(void *ctx) { static int pveh_footer(void *ctx) { vs_footer(" 編輯歷史 ", - " (↑/↓/PgUp/PgDn/0-9)移動 (Enter/r/→)選擇 \t(q/←)跳出"); + " (↑/↓/PgUp/PgDn/0-9)移動 (Enter/r/→)選擇 (~)" RECYCLE_BIN_NAME + "\t(q/←)跳出"); move(b_lines-1, 0); return 0; } -static int -pveh_cursor(int y, int curr, void *ctx) { - // (y, 0) before drawing -#ifdef USE_PFTERM - outs("●\b"); -#else - cursor_show(y, 0); -#endif - return 0; +static void +pveh_solve_rev_filename(int rev, int i, char *fname, size_t sz_fname, + pveh_ctx *cx) { + if (cx->base_as_current && i == 0) + strlcpy(fname, cx->filebase, sz_fname); + else + timecapsule_get_by_revision( + cx->filebase, rev + cx->rev_base, fname, sz_fname); } static int @@ -237,19 +246,31 @@ pveh_renderer(int i, int curr, int total, int rows, void *ctx) { char fname[PATHLEN]; time4_t ftime = 0; pveh_ctx *cx = (pveh_ctx*) ctx; + int rev = total - i; // i/curr = 0 based, rev = 1 based + + if (cx->timestamps[i] == 0) { + pveh_solve_rev_filename(rev, i, fname, sizeof(fname), cx); + ftime = dasht(fname); + if (!ftime) + ftime++; + cx->timestamps[i] = ftime; + } else { + ftime = cx->timestamps[i]; + } - snprintf(fname, sizeof(fname), "%s.%03d", cx->filebase, i); - ftime = dasht(fname); if (ftime != -1) subject = Cdate(&ftime); else subject = "(記錄已過保留期限/已清除)"; - prints(" %s%s 版本: #%08d ", + prints(" %s%s 版本: ", (i == curr) ? ANSI_COLOR(1;41;37) : "", - (ftime == -1) ? ANSI_COLOR(1;30) : "", - i + 1); - prints(" 時間: %-47s" ANSI_RESET "\n", subject); + (ftime == -1) ? ANSI_COLOR(1;30) : ""); + if (cx->base_as_current && i == 0) + outs("[目前版本]"); + else + prints("#%09d", rev + cx->rev_base); + prints(" 時間: %-*s" ANSI_RESET "\n", t_columns - 31, subject); return 0; } @@ -257,27 +278,66 @@ static int pveh_input_processor(int key, int curr, int total, int rows, void *ctx) { char fname[PATHLEN]; pveh_ctx *cx = (pveh_ctx*) ctx; + int rev = total - curr; // see renderer switch (key) { case KEY_ENTER: case KEY_RIGHT: case 'r': - snprintf(fname, sizeof(fname), "%s.%03d", cx->filebase, curr); + pveh_solve_rev_filename(rev, curr, fname, sizeof(fname), cx); more(fname, YEA); return PSB_NOP; + + case '~': + cx->leave_for_recycle_bin = 1; + return PSB_EOF; } return PSB_NA; } +static int +pveh_welcome() { + // warning screen! + static char is_first_enter_pveh = 1; + + if (is_first_enter_pveh) { + is_first_enter_pveh = 0; + clear(); + move(2, 0); + outs(ANSI_COLOR(1;31) +" 歡迎使用 Time Capsule 的編輯歷史瀏覽系統!\n\n" ANSI_RESET +" 提醒您: (1) 此系統尚在實驗性開放中,站方未來會決定是否繼續提供此功\能。\n\n" +" (2) 所有的資料僅供參考,站方不保證此處為完整的電磁記錄。\n\n" +" (3) 所有的資料都可能不定期由系統清除掉。\n" +" 無編輯歷史不能代表沒有編輯過,也可能是被清除了\n\n" +" Mini FAQ:\n\n" +" Q: 怎樣才會有歷史記錄 (增加版本數)?\n" +" A: 在系統更新後每次使用 E 編輯文章並存檔就會有記錄。推文不會增加記錄版本數\n\n" +" Q: 通常歷史會保留多久?\n" +" A: 仍在評估中,或許\會是兩週到一個月\n\n" +" Q: 檔案被刪了也可以看歷史嗎?\n" +" A: 屍體還在看板上可以直接對<本文已被刪除>按,不然就先進" + RECYCLE_BIN_NAME "(~)再找\n" + ); + doupdate(); + pressanykey(); + } + return 0; +} + + int -psb_view_edit_history(const char *base, const char *subject, int max_hist) { +psb_view_edit_history(const char *base, const char *subject, + int maxrev, int current_as_base) { pveh_ctx pvehctx = { .subject = subject, .filebase = base, + .rev_base = 0, + .base_as_current = current_as_base, }; PSB_CTX ctx = { .curr = 0, - .total = max_hist, + .total = maxrev + pvehctx.base_as_current, .header_lines = 3, .footer_lines = 2, .allow_pbs_version_message = 1, @@ -285,35 +345,172 @@ psb_view_edit_history(const char *base, const char *subject, int max_hist) { .header = pveh_header, .footer = pveh_footer, .renderer = pveh_renderer, - .cursor = pveh_cursor, .input_processor = pveh_input_processor, }; + pveh_welcome(); + + if (maxrev > PVEH_LIMIT_NUMBER) { + pvehctx.rev_base = maxrev - PVEH_LIMIT_NUMBER; + ctx.total -= pvehctx.rev_base; + } + + pvehctx.timestamps = (time4_t*) malloc (sizeof(time4_t) * ctx.total); + if (!pvehctx.timestamps) { + vmsgf("內部錯誤,請至" BN_BUGREPORT "看板報告,謝謝"); + return FULLUPDATE; + } + // load on demand! + memset(pvehctx.timestamps, 0, sizeof(time4_t) * ctx.total); + + psb_main(&ctx); + free(pvehctx.timestamps); + return (pvehctx.leave_for_recycle_bin ? + RET_RECYCLEBIN : + FULLUPDATE); +} + +/////////////////////////////////////////////////////////////////////////// +// Time Capsule: Recycle Bin +#define PVRB_LIMIT_NUMBER (1000) + +typedef struct { + const char *dirbase; + const char *subject; + int viewbase; + fileheader_t *records; +} pvrb_ctx; + +static int +pvrb_header(void *ctx) { + pvrb_ctx *cx = (pvrb_ctx*) ctx; + vs_hdr2barf(" 【" TIME_CAPSULE_NAME ": " RECYCLE_BIN_NAME "】 \t %s", + cx->subject); + move(1, 0); + outs("請注意此處的檔案將不定期清除。"); + outs("\n"); + return 0; +} + +static int +pvrb_footer(void *ctx) { + vs_footer(" 已刪檔案 ", + " (↑/↓/PgUp/PgDn/0-9)移動 (Enter/r/→)選擇 \t(q/←)跳出"); + move(b_lines-1, 0); + return 0; +} + +static int +pvrb_renderer(int i, int curr, int total, int rows, void *ctx) { + pvrb_ctx *cx = (pvrb_ctx*) ctx; + fileheader_t *fh = &cx->records[total - i - 1]; + + // quick display, but lack of recommend counter... + prints(" %s %06d %-5.5s %-12.12s %-*.*s" ANSI_RESET "\n", + (i == curr) ? ANSI_COLOR(46;30) : "", + i+1, fh->date, fh->owner, t_columns-33, t_columns-33, fh->title); + return 0; +} + +static int +pvrb_input_processor(int key, int curr, int total, int rows, void *ctx) { + char fname[PATHLEN]; + int maxrev; + pvrb_ctx *cx = (pvrb_ctx*) ctx; + fileheader_t *fh = &cx->records[total - curr - 1]; + + switch (key) { + case KEY_ENTER: + case KEY_RIGHT: + case 'r': + setdirpath(fname, cx->dirbase, fh->filename); + maxrev = timecapsule_get_max_revision_number(fname); + if (maxrev == 1) { + char revfname[PATHLEN]; + timecapsule_get_by_revision( + fname, 1, revfname, sizeof(revfname)); + more(revfname, YEA); + } else if (maxrev > 1) { + psb_view_edit_history(fname, fh->title, maxrev, 0); + } else { + vmsg("抱歉,本文歷史資料已被系統清除。"); + } + return PSB_NOP; + } + return PSB_NA; +} + +static int +pvrb_welcome() { // warning screen! - static char is_first_enter_pveh = 1; - if (is_first_enter_pveh) { - is_first_enter_pveh = 0; - clear(); + static char is_first_enter_pvrb = 1; + + if (is_first_enter_pvrb) { + is_first_enter_pvrb = 0; + clear(); SOLVE_ANSI_CACHE(); move(2, 0); - outs( -" 歡迎使用文章編輯歷史瀏覽系統!\n\n" + outs(ANSI_COLOR(1;36) +" 歡迎使用 " TIME_CAPSULE_NAME " " RECYCLE_BIN_NAME "!\n\n" ANSI_RESET " 提醒您: (1) 此系統尚在實驗性開放中,站方未來會決定是否繼續提供此功\能。\n\n" " (2) 所有的資料僅供參考,站方不保證此處為完整的電磁記錄。\n\n" " (3) 所有的資料都可能不定期由系統清除掉。\n" -" 無編輯歷史不能代表沒有編輯過,也可能是被清除了\n\n" +" 大D 與 ^D 刪除的內容不會保留。\n\n" " Mini FAQ:\n\n" -" Q: 怎樣才會有歷史記錄 (增加版本數)?\n" -" A: 在系統更新後每次使用 E 編輯文章並存檔就會有記錄。推文不會增加記錄版本數\n\n" -" Q: 通常歷史會保留多久?\n" -" A: 仍在評估中,或許\會是一週到一個月以上\n\n" -" Q: 檔案被刪了也可以看歷史嗎?\n" -" A: 屍體還在看板上就可以(直接對<本文已被刪除>按),但在 deleted 看板是無效的\n" +" Q: 通常檔案會保留多久?\n" +" A: 仍在評估中,或許\會是兩週到一個月,另外篇數也會有上限。\n\n" +" Q: 哪些地方有回收筒可用?\n" +" A: 目前開放個人信箱(所有用戶)跟看板(板主限定)。 精華區暫不支援。\n\n" ); + doupdate(); pressanykey(); } - + return 0; +} +int +psb_recycle_bin(const char *base, const char *title) { + int nrecords = 0, viewbase = 0; + pvrb_ctx pvrbctx = { + .dirbase = base, + .subject = title, + }; + PSB_CTX ctx = { + .curr = 0, + .total = 0, // maxrev + pvrbctx.base_as_current, + .header_lines = 3, + .footer_lines = 2, + .allow_pbs_version_message = 1, + .ctx = (void*)&pvrbctx, + .header = pvrb_header, + .footer = pvrb_footer, + .renderer = pvrb_renderer, + .input_processor = pvrb_input_processor, + }; + + nrecords = timecapsule_get_max_archive_number(base, sizeof(fileheader_t)); + if (!nrecords) { + vmsg("目前" RECYCLE_BIN_NAME "內無任何內容。"); + return 0; + } + + pvrb_welcome(); + + // truncate on large size + if (nrecords > PVRB_LIMIT_NUMBER) { + viewbase = nrecords - PVRB_LIMIT_NUMBER; + nrecords -= viewbase; + } + ctx.total = nrecords; + + pvrbctx.records = (fileheader_t*) malloc (sizeof(fileheader_t) * nrecords); + if (!pvrbctx.records) { + vmsgf("內部錯誤,請至" BN_BUGREPORT "看板報告,謝謝"); + return 0; + } + timecapsule_get_archive_blobs(base, viewbase, nrecords, pvrbctx.records, + sizeof(fileheader_t)); psb_main(&ctx); + free(pvrbctx.records); return 0; } @@ -357,12 +554,6 @@ pae_renderer(int i, int curr, int total, int rows, void *ctx) { } static int -pae_cursor(int y, int curr, void *ctx) { - cursor_show(y, 0); - return 0; -} - -static int pae_input_processor(int key, int curr, int total, int rows, void *ctx) { int result; pae_ctx *cx = (pae_ctx*) ctx; @@ -417,7 +608,6 @@ psb_admin_edit() { .header = pae_header, .footer = pae_footer, .renderer = pae_renderer, - .cursor = pae_cursor, .input_processor = pae_input_processor, }; diff --git a/pttbbs/mbbsd/read.c b/pttbbs/mbbsd/read.c index 97312ae7..022a3e77 100644 --- a/pttbbs/mbbsd/read.c +++ b/pttbbs/mbbsd/read.c @@ -179,7 +179,8 @@ TagPruner(int bid) if (vans("刪除所有標記[N]?") != 'y') return READ_REDRAW; #ifdef SAFE_ARTICLE_DELETE - if(bp && !(currmode & MODE_DIGEST) && bp->nuser > 30) + if(bp && !(currmode & MODE_DIGEST) && + bp->nuser >= SAFE_ARTICLE_DELETE_NUSER) safe_delete_range(currdirect, 0, 0); else #endif diff --git a/pttbbs/mbbsd/timecap.c b/pttbbs/mbbsd/timecap.c new file mode 100644 index 00000000..9e8dae39 --- /dev/null +++ b/pttbbs/mbbsd/timecap.c @@ -0,0 +1,339 @@ +/* $Id $ */ +#include "bbs.h" + +// Time Capsule / Magical Index +// +// Management of edit history and deleted (archived) objects +// +// Author: Hung-Te Lin (piaip) +// -------------------------------------------------------------------------- +// Copyright (c) 2010 Hung-Te Lin <piaip@csie.ntu.edu.tw> +// All rights reserved. +// Distributed under BSD license (GPL compatible). +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// -------------------------------------------------------------------------- +// +// The Time Capsule provides an interface to "append" objects into a managed +// data pool. There's no diff, compress, incremental, or expiring because the +// time capsule is designed to be very fast and least impact to original system. +// You should do those by a post-processing cron job (ex, compress or remove +// files periodically). +// +// Currently any objects can perform two kinds of actions in Time Capsule: +// - Revision: Record a new by with numerical (auto-inc) number. +// - Archive: Append blob information in an index file +// +// For BBS, +// editing posts should perform a Revision +// deleting posts should perform a Revision and then Archive. +// +// NOTE: Revision index is now using "add one byte for each rev", that helps us +// to get the rev by dashs instead of reading content. However that only +// works fine if the revisions are small. The statistics shown that most +// revisions are less than 100 so it's working fine for a traditional BBS, +// but not for a wiki-like system. + +/////////////////////////////////////////////////////////////////////////// +// Constants + +enum TIME_CAPSULE_ACTION_TYPE { + TIME_CAPSULE_ACTION_REVISION = 1, + TIME_CAPSULE_ACTION_ARCHIVE, +}; + +#define TIME_CAPSULE_BASE_FOLDER_NAME ".timecap/" +#define TIME_CAPSULE_ARCHIVE_INDEX_NAME "archive.idx" +#define TIME_CAPSULE_REVISION_INDEX_NAME ".rev" + +/////////////////////////////////////////////////////////////////////////// +// Core Function + +static int +timecap_get_max_revision(const char *capsule_index) { + off_t sz = dashs(capsule_index); + // TODO we can change implementation to read int instead of dashs + return sz > 0 ? (int)sz : 0; +} + +static int +timecap_get_max_archive(const char *archive_index, size_t ref_blob_size) { + off_t sz = dashs(archive_index); + if (!ref_blob_size || sz < 1) + return 0; + // TODO what if ref_blob_size%sz != 0 ? + // Maybe we should keep some metadata in the archive in the future + return sz / ref_blob_size; +} + +static int +timecap_get_archive_content(const char *archive_index, int index, int nblobs, + void *blobsptr, size_t szblob) { + int fd; + off_t srcsz = dashs(archive_index), + offset = (off_t) index * szblob, + readsz = nblobs * szblob; + + assert(blobsptr && szblob && nblobs); + if (offset < 0 || srcsz < offset || srcsz < offset + readsz) + return 0; + + fd = open(archive_index, O_RDONLY); + if (fd < 0) + return 0; + if (lseek(fd, offset, SEEK_SET) != offset || + read(fd, (char*)blobsptr, readsz) != readsz) { + close(fd); + return 0; + } + close(fd); + return 1; +} + +static int +timecap_convert_revision_filename(int rev, + char *rev_index_path, + size_t sz_index_path) { + char revstr[STRLEN]; + char *s = strrchr(rev_index_path, '.'); + if (!s++) + return 0; + + *s = 0; + sprintf(revstr, "%03d", rev); + strlcat(rev_index_path, revstr, sz_index_path); + return 1; +} + +static int +timecap_add_revision(const char *object_path, const char *capsule_index) { + int rev, fd; + char capsule_path[PATHLEN]; + const char nul = 0; + + strlcpy(capsule_path, capsule_index, sizeof(capsule_path)); + // solve index and revision + rev = timecap_get_max_revision(capsule_index); + if (rev++ < 0) + rev = 1; + assert(rev != 0); + + timecap_convert_revision_filename(rev, capsule_path, sizeof(capsule_path)); + fd = OpenCreate(capsule_index, O_WRONLY | O_EXLOCK | O_APPEND); + if (fd < 0) + return 0; + + // we don't use Link because time capsule are supposed to not + // causing extra disk usage. + if (link(object_path, capsule_path) != 0 || + write(fd, &nul, 1) != 1) { + close(fd); + return 0; + } + + close(fd); + return rev; +} + +static int +timecap_add_archive(const char *capsule_index, + const void *ref_blob, size_t ref_blob_size) +{ + int fd; + + if (!ref_blob || !ref_blob_size) + return 0; + + // check if the index file is broken or incompatible. + // if (dashs(capsule_index) % ref_blob_size) + // return 0; + + // solve new store name + fd = OpenCreate(capsule_index, O_WRONLY | O_EXLOCK | O_APPEND); + if (fd < 0) + return 0; + + // the blob must provide enough information to get the object name. + if (write(fd, ref_blob, ref_blob_size) != ref_blob_size) { + close(fd); + return 0; + } + close(fd); + return 1; +} + +static int +timecap_solve_base_folder(int create, const char *object_path, + char *folder, size_t sz_folder) { + // currently we only support file objects + if (create && !dashf(object_path)) + return 0; + + assert(sz_folder >= PATHLEN); // default size in setdirpath + setdirpath(folder, object_path, TIME_CAPSULE_BASE_FOLDER_NAME); + if (!dashd(folder) && (!create || Mkdir(folder) != 0)) + return 0; + + return 1; +} + +static int +timecap_solve_revision_index(int create, const char *object_path, + char *index_path, size_t sz_index_path) { + const char *object_name = strrchr(object_path, '/'); + if (!object_name++ || !*object_name) + return 0; + + if (!timecap_solve_base_folder(create, object_path, + index_path, sz_index_path)) + return 0; + + strlcat(index_path, object_name, sz_index_path); + strlcat(index_path, TIME_CAPSULE_REVISION_INDEX_NAME, sz_index_path); + return 1; +} + +static int +timecap_solve_archive_index(int create, const char *object_path, + char *index_path, size_t sz_index_path) { + if (!timecap_solve_base_folder(create, object_path, + index_path, sz_index_path)) + return 0; + + strlcat(index_path, TIME_CAPSULE_ARCHIVE_INDEX_NAME, sz_index_path); + return 1; +} + +static int +timecap_add_object(const char *object_path, + enum TIME_CAPSULE_ACTION_TYPE action_type, + const void *ref_blob, + size_t ref_blob_size) { + char capsule_index[PATHLEN]; + + // make sure the base folder can be created. + switch (action_type) { + case TIME_CAPSULE_ACTION_REVISION: + if (!timecap_solve_revision_index(1, object_path, capsule_index, + sizeof(capsule_index))) + return 0; + return timecap_add_revision(object_path, capsule_index); + + case TIME_CAPSULE_ACTION_ARCHIVE: + if (!timecap_solve_archive_index(1, object_path, capsule_index, + sizeof(capsule_index))) + return 0; + return timecap_add_archive(capsule_index, ref_blob, ref_blob_size); + } + assert(!"unknown time capsule reference type"); + return 0; +} + +int +timecap_query_object_max_number(const char *object_path, + enum TIME_CAPSULE_ACTION_TYPE action_type, + size_t ref_blob_size) { + char capsule_index[PATHLEN]; + + switch(action_type) { + case TIME_CAPSULE_ACTION_REVISION: + if (!timecap_solve_revision_index(0, object_path, capsule_index, + sizeof(capsule_index))) + return 0; + return timecap_get_max_revision(capsule_index); + + case TIME_CAPSULE_ACTION_ARCHIVE: + if (!timecap_solve_archive_index(0, object_path, capsule_index, + sizeof(capsule_index))) + return 0; + return timecap_get_max_archive(capsule_index, ref_blob_size); + } + assert(!"unknown time capsule reference type"); + return 0; +} + +int +timecap_query_object_refblobs(const char *object_path, + int index, + int nblobs, + void *ref_blob, + size_t ref_blob_size) { + char capsule_index[PATHLEN]; + + if (!timecap_solve_archive_index(0, object_path, capsule_index, + sizeof(capsule_index))) + return 0; + return timecap_get_archive_content( + capsule_index, index, nblobs, ref_blob, ref_blob_size); +} + +/////////////////////////////////////////////////////////////////////////// +// Export API + +int +timecapsule_add_revision(const char *filename) { + return timecap_add_object(filename, TIME_CAPSULE_ACTION_REVISION, NULL, 0); +} + +int +timecapsule_get_max_revision_number(const char *filename) { + return timecap_query_object_max_number( + filename, TIME_CAPSULE_ACTION_REVISION, 0); +} + +int +timecapsule_get_max_archive_number(const char *filename, size_t szrefblob) { + return timecap_query_object_max_number( + filename, TIME_CAPSULE_ACTION_ARCHIVE, szrefblob); +} + +int +timecapsule_archive(const char *filename, const void *ref, size_t szref) { + return timecap_add_object( + filename, TIME_CAPSULE_ACTION_ARCHIVE, ref, szref); +} + +int +timecapsule_get_by_revision(const char *filename, int rev, + char *rev_path, size_t sz_rev_path) { + if (!timecap_solve_revision_index(0, filename, rev_path, sz_rev_path)) + return 0; + timecap_convert_revision_filename(rev, rev_path, sz_rev_path); + return 1; +} + +int +timecapsule_get_archive_blobs(const char *filename, int idx, int nblobs, + void *blobsptr, size_t szblob) { + return timecap_query_object_refblobs( + filename, idx, nblobs, blobsptr, szblob); +} + +int +timecapsule_archive_new_revision(const char *filename, + const void *ref, size_t szref, + char *archived_path, size_t sz_archived_path) { + char capsule_index[PATHLEN]; + int rev; + + if (!timecap_solve_revision_index(1, filename, capsule_index, + sizeof(capsule_index))) + return 0; + rev = timecap_add_revision(filename, capsule_index); + if (!rev) + return 0; + + if (archived_path) { + strlcpy(archived_path, capsule_index, sz_archived_path); + timecap_convert_revision_filename(rev, archived_path, sz_archived_path); + } + return timecapsule_archive(filename, ref, szref); +} + |