Fossil SCM

Refactor /wikiedit's attachment-list-as-JSON routine to work with other artifact types for re-use elsewhere. Teach /wikiedit's attachment list to use the new file-attach interface.

stephan 2026-06-04 10:07 UTC attach-v2
Commit 275c586426871f130682f2e6e5f377984e46036c5179996dfdbb0740702de47c
+90
--- src/attach.c
+++ src/attach.c
@@ -1446,5 +1446,95 @@
14461446
db_reset(&q);
14471447
}
14481448
db_finalize(&q);
14491449
}
14501450
1451
+/*
1452
+** Renders the list of attachments for artifact pManifest as JSON to
1453
+** blob pOut. If pManifest->type is not one of (CFTYPE_TICKET,
1454
+** CFTYPE_FORUM, CFTYPE_EVENT, CFTYPE_WIKI) then it behaves as if the
1455
+** result set is empty.
1456
+**
1457
+** If there are no matching attachments then its behavior depends on
1458
+** emptyPolicy:
1459
+**
1460
+** <0 = emit a JSON NULL
1461
+** 0 = emit no output
1462
+** >0 = emit an empty JSON array
1463
+**
1464
+** If bLatestOnly is true then only the most recent entry for a given
1465
+** attachment is emitted, else all versions are emitted in descending
1466
+** mtime order.
1467
+**
1468
+** Returns the number of attachments.
1469
+**
1470
+** Output format:
1471
+**
1472
+** [{
1473
+** "uuid": attachment artifact hash,
1474
+** "src": hash of the attachment blob,
1475
+** "target": wiki page name or ticket/event ID,
1476
+** "filename": filename of attachment,
1477
+** "mtime": ISO-8601 timestamp UTC,
1478
+** "isLatest": true if this is the latest version of this file
1479
+** else false,
1480
+** }, ...once per attachment]
1481
+**
1482
+*/
1483
+int attachments_to_json(const Manifest *pManifest,
1484
+ Blob *pOut, int bLatestOnly,
1485
+ int emptyPolicy){
1486
+ int i = 0;
1487
+ Stmt q = empty_Stmt;
1488
+ char *zToFree = 0;
1489
+ const char *zTgt = 0;
1490
+ switch(pManifest->type){
1491
+ case CFTYPE_FORUM: zTgt = zToFree = rid_to_uuid(pManifest->rid);
1492
+ break;
1493
+ case CFTYPE_WIKI: zTgt = pManifest->zWikiTitle; break;
1494
+ case CFTYPE_EVENT: zTgt = pManifest->zEventId; break;
1495
+ case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break;
1496
+ default:
1497
+ goto empty_result;
1498
+ }
1499
+ db_prepare(&q,
1500
+ "SELECT datetime(mtime), src, target, filename, isLatest,"
1501
+ " (SELECT uuid FROM blob WHERE rid=attachid) uuid"
1502
+ " FROM attachment"
1503
+ " WHERE target=%Q"
1504
+ " AND (isLatest OR %d)"
1505
+ " ORDER BY target, isLatest DESC, mtime DESC",
1506
+ zTgt, !bLatestOnly
1507
+ );
1508
+ while(SQLITE_ROW == db_step(&q)){
1509
+ const char * zTime = db_column_text(&q, 0);
1510
+ const char * zSrc = db_column_text(&q, 1);
1511
+ const char * zTarget = db_column_text(&q, 2);
1512
+ const char * zName = db_column_text(&q, 3);
1513
+ const int isLatest = db_column_int(&q, 4);
1514
+ const char * zUuid = db_column_text(&q, 5);
1515
+ if(!i++){
1516
+ blob_append_char(pOut, '[');
1517
+ }else{
1518
+ blob_append_char(pOut, ',');
1519
+ }
1520
+ blob_appendf(
1521
+ pOut,
1522
+ "{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
1523
+ "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
1524
+ zUuid, zSrc, zTarget,
1525
+ zName, zTime, isLatest ? "true" : "false");
1526
+ }
1527
+ fossil_free(zToFree);
1528
+ db_finalize(&q);
1529
+ if(!i){
1530
+ empty_result:
1531
+ if( emptyPolicy>0 ){
1532
+ blob_append_literal(pOut, "[]");
1533
+ }else if( emptyPolicy<0 ){
1534
+ blob_append_literal(pOut, "null");
1535
+ }
1536
+ }else{
1537
+ blob_append_char(pOut, ']');
1538
+ }
1539
+ return i;
1540
+}
14511541
--- src/attach.c
+++ src/attach.c
@@ -1446,5 +1446,95 @@
1446 db_reset(&q);
1447 }
1448 db_finalize(&q);
1449 }
1450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1451
--- src/attach.c
+++ src/attach.c
@@ -1446,5 +1446,95 @@
1446 db_reset(&q);
1447 }
1448 db_finalize(&q);
1449 }
1450
1451 /*
1452 ** Renders the list of attachments for artifact pManifest as JSON to
1453 ** blob pOut. If pManifest->type is not one of (CFTYPE_TICKET,
1454 ** CFTYPE_FORUM, CFTYPE_EVENT, CFTYPE_WIKI) then it behaves as if the
1455 ** result set is empty.
1456 **
1457 ** If there are no matching attachments then its behavior depends on
1458 ** emptyPolicy:
1459 **
1460 ** <0 = emit a JSON NULL
1461 ** 0 = emit no output
1462 ** >0 = emit an empty JSON array
1463 **
1464 ** If bLatestOnly is true then only the most recent entry for a given
1465 ** attachment is emitted, else all versions are emitted in descending
1466 ** mtime order.
1467 **
1468 ** Returns the number of attachments.
1469 **
1470 ** Output format:
1471 **
1472 ** [{
1473 ** "uuid": attachment artifact hash,
1474 ** "src": hash of the attachment blob,
1475 ** "target": wiki page name or ticket/event ID,
1476 ** "filename": filename of attachment,
1477 ** "mtime": ISO-8601 timestamp UTC,
1478 ** "isLatest": true if this is the latest version of this file
1479 ** else false,
1480 ** }, ...once per attachment]
1481 **
1482 */
1483 int attachments_to_json(const Manifest *pManifest,
1484 Blob *pOut, int bLatestOnly,
1485 int emptyPolicy){
1486 int i = 0;
1487 Stmt q = empty_Stmt;
1488 char *zToFree = 0;
1489 const char *zTgt = 0;
1490 switch(pManifest->type){
1491 case CFTYPE_FORUM: zTgt = zToFree = rid_to_uuid(pManifest->rid);
1492 break;
1493 case CFTYPE_WIKI: zTgt = pManifest->zWikiTitle; break;
1494 case CFTYPE_EVENT: zTgt = pManifest->zEventId; break;
1495 case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break;
1496 default:
1497 goto empty_result;
1498 }
1499 db_prepare(&q,
1500 "SELECT datetime(mtime), src, target, filename, isLatest,"
1501 " (SELECT uuid FROM blob WHERE rid=attachid) uuid"
1502 " FROM attachment"
1503 " WHERE target=%Q"
1504 " AND (isLatest OR %d)"
1505 " ORDER BY target, isLatest DESC, mtime DESC",
1506 zTgt, !bLatestOnly
1507 );
1508 while(SQLITE_ROW == db_step(&q)){
1509 const char * zTime = db_column_text(&q, 0);
1510 const char * zSrc = db_column_text(&q, 1);
1511 const char * zTarget = db_column_text(&q, 2);
1512 const char * zName = db_column_text(&q, 3);
1513 const int isLatest = db_column_int(&q, 4);
1514 const char * zUuid = db_column_text(&q, 5);
1515 if(!i++){
1516 blob_append_char(pOut, '[');
1517 }else{
1518 blob_append_char(pOut, ',');
1519 }
1520 blob_appendf(
1521 pOut,
1522 "{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
1523 "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
1524 zUuid, zSrc, zTarget,
1525 zName, zTime, isLatest ? "true" : "false");
1526 }
1527 fossil_free(zToFree);
1528 db_finalize(&q);
1529 if(!i){
1530 empty_result:
1531 if( emptyPolicy>0 ){
1532 blob_append_literal(pOut, "[]");
1533 }else if( emptyPolicy<0 ){
1534 blob_append_literal(pOut, "null");
1535 }
1536 }else{
1537 blob_append_char(pOut, ']');
1538 }
1539 return i;
1540 }
1541
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -485,14 +485,16 @@
485485
return i;
486486
}
487487
}/*Attacher*/;
488488
F.Attacher = Attacher;
489489
490
- const eFormWrapper = document.querySelector('#attachadd-form-wrapper');
491
- if( eFormWrapper ){
492
- /* Inject a file-attachment form. */
493
- eFormWrapper.classList.remove('hidden');
490
+ const eAttachWrapper = document.querySelector('#attachadd-form-wrapper');
491
+ if( eAttachWrapper ){
492
+ /* This page is /attachadd v2 or a workalike. eAttachWrapper holds
493
+ input[type=hidden] fields for use in attaching files and is
494
+ where we inject a file attachment widget. */
495
+ eAttachWrapper.classList.remove('hidden');
494496
const urlArgs = new URLSearchParams(window.location.search);
495497
let zTarget = urlArgs.get('target');
496498
let zTo = urlArgs.get('to') || urlArgs.get('from');
497499
const eBtnSubmit = D.button("Submit");
498500
eBtnSubmit.type = 'button';
@@ -506,11 +508,11 @@
506508
const cbAttacherChange = (ev)=>{
507509
const a = ev.detail.attacher;
508510
updateBtnSubmit(a);
509511
};
510512
const att = new Attacher({
511
- container: eFormWrapper,
513
+ container: eAttachWrapper,
512514
startWith: 1,
513515
listener: cbAttacherChange,
514516
controls: [eBtnSubmit],
515517
description: false
516518
});
@@ -526,12 +528,12 @@
526528
for(const row of li){
527529
++i;
528530
fd.append('file'+i, row.content);
529531
if( row.description ) fd.append('file'+i+'_desc', row.description);
530532
}
531
- for( const eIn of eFormWrapper.querySelectorAll(
532
- ':scope > input[type="hidden"]'
533
+ for( const eIn of eAttachWrapper.querySelectorAll(
534
+ 'input[type="hidden"]'
533535
) ){
534536
/* Copy over hidden input fields emitted by the server. */
535537
if( eIn.name==='target' ){
536538
zTarget = eIn.value;
537539
}else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){
538540
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -485,14 +485,16 @@
485 return i;
486 }
487 }/*Attacher*/;
488 F.Attacher = Attacher;
489
490 const eFormWrapper = document.querySelector('#attachadd-form-wrapper');
491 if( eFormWrapper ){
492 /* Inject a file-attachment form. */
493 eFormWrapper.classList.remove('hidden');
 
 
494 const urlArgs = new URLSearchParams(window.location.search);
495 let zTarget = urlArgs.get('target');
496 let zTo = urlArgs.get('to') || urlArgs.get('from');
497 const eBtnSubmit = D.button("Submit");
498 eBtnSubmit.type = 'button';
@@ -506,11 +508,11 @@
506 const cbAttacherChange = (ev)=>{
507 const a = ev.detail.attacher;
508 updateBtnSubmit(a);
509 };
510 const att = new Attacher({
511 container: eFormWrapper,
512 startWith: 1,
513 listener: cbAttacherChange,
514 controls: [eBtnSubmit],
515 description: false
516 });
@@ -526,12 +528,12 @@
526 for(const row of li){
527 ++i;
528 fd.append('file'+i, row.content);
529 if( row.description ) fd.append('file'+i+'_desc', row.description);
530 }
531 for( const eIn of eFormWrapper.querySelectorAll(
532 ':scope > input[type="hidden"]'
533 ) ){
534 /* Copy over hidden input fields emitted by the server. */
535 if( eIn.name==='target' ){
536 zTarget = eIn.value;
537 }else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){
538
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -485,14 +485,16 @@
485 return i;
486 }
487 }/*Attacher*/;
488 F.Attacher = Attacher;
489
490 const eAttachWrapper = document.querySelector('#attachadd-form-wrapper');
491 if( eAttachWrapper ){
492 /* This page is /attachadd v2 or a workalike. eAttachWrapper holds
493 input[type=hidden] fields for use in attaching files and is
494 where we inject a file attachment widget. */
495 eAttachWrapper.classList.remove('hidden');
496 const urlArgs = new URLSearchParams(window.location.search);
497 let zTarget = urlArgs.get('target');
498 let zTo = urlArgs.get('to') || urlArgs.get('from');
499 const eBtnSubmit = D.button("Submit");
500 eBtnSubmit.type = 'button';
@@ -506,11 +508,11 @@
508 const cbAttacherChange = (ev)=>{
509 const a = ev.detail.attacher;
510 updateBtnSubmit(a);
511 };
512 const att = new Attacher({
513 container: eAttachWrapper,
514 startWith: 1,
515 listener: cbAttacherChange,
516 controls: [eBtnSubmit],
517 description: false
518 });
@@ -526,12 +528,12 @@
528 for(const row of li){
529 ++i;
530 fd.append('file'+i, row.content);
531 if( row.description ) fd.append('file'+i+'_desc', row.description);
532 }
533 for( const eIn of eAttachWrapper.querySelectorAll(
534 'input[type="hidden"]'
535 ) ){
536 /* Copy over hidden input fields emitted by the server. */
537 if( eIn.name==='target' ){
538 zTarget = eIn.value;
539 }else if( eIn.name==='to' || (eIn.name==='from' && !zTo) ){
540
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1187,11 +1187,11 @@
11871187
if(!wi.attachments || !wi.attachments.length){
11881188
D.append(f.eAttach,
11891189
btnReload,
11901190
" No attachments found for page ["+wi.name+"]. ",
11911191
D.a(F.repoUrl('attachadd',{
1192
- page: wi.name,
1192
+ target: wi.name,
11931193
from: F.repoUrl('wikiedit',{name: wi.name})}),
11941194
"Add attachments..." )
11951195
);
11961196
return this;
11971197
}
@@ -1198,14 +1198,14 @@
11981198
D.append(
11991199
f.eAttach,
12001200
D.append(D.p(),
12011201
btnReload," ",
12021202
D.a(F.repoUrl('attachlist',{page:wi.name}),
1203
- "Attachments for page ["+wi.name+"]."),
1204
- " ",
1203
+ "Attachments for page ["+wi.name+"]"),
1204
+ ". ",
12051205
D.a(F.repoUrl('attachadd',{
1206
- page:wi.name,
1206
+ target:wi.name,
12071207
from: F.repoUrl('wikiedit',{name: wi.name})}),
12081208
"Add attachments..." )
12091209
)
12101210
);
12111211
wi.attachments.forEach(function(a){
12121212
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1187,11 +1187,11 @@
1187 if(!wi.attachments || !wi.attachments.length){
1188 D.append(f.eAttach,
1189 btnReload,
1190 " No attachments found for page ["+wi.name+"]. ",
1191 D.a(F.repoUrl('attachadd',{
1192 page: wi.name,
1193 from: F.repoUrl('wikiedit',{name: wi.name})}),
1194 "Add attachments..." )
1195 );
1196 return this;
1197 }
@@ -1198,14 +1198,14 @@
1198 D.append(
1199 f.eAttach,
1200 D.append(D.p(),
1201 btnReload," ",
1202 D.a(F.repoUrl('attachlist',{page:wi.name}),
1203 "Attachments for page ["+wi.name+"]."),
1204 " ",
1205 D.a(F.repoUrl('attachadd',{
1206 page:wi.name,
1207 from: F.repoUrl('wikiedit',{name: wi.name})}),
1208 "Add attachments..." )
1209 )
1210 );
1211 wi.attachments.forEach(function(a){
1212
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1187,11 +1187,11 @@
1187 if(!wi.attachments || !wi.attachments.length){
1188 D.append(f.eAttach,
1189 btnReload,
1190 " No attachments found for page ["+wi.name+"]. ",
1191 D.a(F.repoUrl('attachadd',{
1192 target: wi.name,
1193 from: F.repoUrl('wikiedit',{name: wi.name})}),
1194 "Add attachments..." )
1195 );
1196 return this;
1197 }
@@ -1198,14 +1198,14 @@
1198 D.append(
1199 f.eAttach,
1200 D.append(D.p(),
1201 btnReload," ",
1202 D.a(F.repoUrl('attachlist',{page:wi.name}),
1203 "Attachments for page ["+wi.name+"]"),
1204 ". ",
1205 D.a(F.repoUrl('attachadd',{
1206 target:wi.name,
1207 from: F.repoUrl('wikiedit',{name: wi.name})}),
1208 "Add attachments..." )
1209 )
1210 );
1211 wi.attachments.forEach(function(a){
1212
+5 -39
--- src/wiki.c
+++ src/wiki.c
@@ -830,49 +830,15 @@
830830
** mtime order.
831831
*/
832832
static void wiki_ajax_emit_page_attachments(Manifest * pWiki,
833833
int latestOnly,
834834
int nullIfEmpty){
835
- int i = 0;
836
- Stmt q = empty_Stmt;
837
- db_prepare(&q,
838
- "SELECT datetime(mtime), src, target, filename, isLatest,"
839
- " (SELECT uuid FROM blob WHERE rid=attachid) uuid"
840
- " FROM attachment"
841
- " WHERE target=%Q"
842
- " AND (isLatest OR %d)"
843
- " ORDER BY target, isLatest DESC, mtime DESC",
844
- pWiki->zWikiTitle, !latestOnly
845
- );
846
- while(SQLITE_ROW == db_step(&q)){
847
- const char * zTime = db_column_text(&q, 0);
848
- const char * zSrc = db_column_text(&q, 1);
849
- const char * zTarget = db_column_text(&q, 2);
850
- const char * zName = db_column_text(&q, 3);
851
- const int isLatest = db_column_int(&q, 4);
852
- const char * zUuid = db_column_text(&q, 5);
853
- if(!i++){
854
- CX("[");
855
- }else{
856
- CX(",");
857
- }
858
- CX("{");
859
- CX("\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
860
- "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
861
- zUuid, zSrc, zTarget,
862
- zName, zTime, isLatest ? "true" : "false");
863
- }
864
- db_finalize(&q);
865
- if(!i){
866
- if(nullIfEmpty){
867
- CX("null");
868
- }else{
869
- CX("[]");
870
- }
871
- }else{
872
- CX("]");
873
- }
835
+ Blob b = BLOB_INITIALIZER;
836
+ attachments_to_json(pWiki, &b, latestOnly,
837
+ nullIfEmpty ? -1 : 1);
838
+ CX("%b", &b);
839
+ blob_reset(&b);
874840
}
875841
876842
/*
877843
** Proxy for wiki_ajax_emit_page_attachments() which attempts to load
878844
** the given wiki page artifact. Returns true if it can load the given
879845
--- src/wiki.c
+++ src/wiki.c
@@ -830,49 +830,15 @@
830 ** mtime order.
831 */
832 static void wiki_ajax_emit_page_attachments(Manifest * pWiki,
833 int latestOnly,
834 int nullIfEmpty){
835 int i = 0;
836 Stmt q = empty_Stmt;
837 db_prepare(&q,
838 "SELECT datetime(mtime), src, target, filename, isLatest,"
839 " (SELECT uuid FROM blob WHERE rid=attachid) uuid"
840 " FROM attachment"
841 " WHERE target=%Q"
842 " AND (isLatest OR %d)"
843 " ORDER BY target, isLatest DESC, mtime DESC",
844 pWiki->zWikiTitle, !latestOnly
845 );
846 while(SQLITE_ROW == db_step(&q)){
847 const char * zTime = db_column_text(&q, 0);
848 const char * zSrc = db_column_text(&q, 1);
849 const char * zTarget = db_column_text(&q, 2);
850 const char * zName = db_column_text(&q, 3);
851 const int isLatest = db_column_int(&q, 4);
852 const char * zUuid = db_column_text(&q, 5);
853 if(!i++){
854 CX("[");
855 }else{
856 CX(",");
857 }
858 CX("{");
859 CX("\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
860 "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
861 zUuid, zSrc, zTarget,
862 zName, zTime, isLatest ? "true" : "false");
863 }
864 db_finalize(&q);
865 if(!i){
866 if(nullIfEmpty){
867 CX("null");
868 }else{
869 CX("[]");
870 }
871 }else{
872 CX("]");
873 }
874 }
875
876 /*
877 ** Proxy for wiki_ajax_emit_page_attachments() which attempts to load
878 ** the given wiki page artifact. Returns true if it can load the given
879
--- src/wiki.c
+++ src/wiki.c
@@ -830,49 +830,15 @@
830 ** mtime order.
831 */
832 static void wiki_ajax_emit_page_attachments(Manifest * pWiki,
833 int latestOnly,
834 int nullIfEmpty){
835 Blob b = BLOB_INITIALIZER;
836 attachments_to_json(pWiki, &b, latestOnly,
837 nullIfEmpty ? -1 : 1);
838 CX("%b", &b);
839 blob_reset(&b);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
840 }
841
842 /*
843 ** Proxy for wiki_ajax_emit_page_attachments() which attempts to load
844 ** the given wiki page artifact. Returns true if it can load the given
845

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button