Fossil SCM

Add attachment size info to the attachments-as-json state and filter empty files from that list (the UI has never allowed empty attachments and size 0 denotes 'deleted'). Add attachment_resolve_target().

stephan 2026-06-04 11:40 UTC attach-v2
Commit 3bc2b92104c3518a2e9f26c117eb0e89d56b84ba8f2955ee7aca8afe5e53cbac
1 file changed +130 -19
+130 -19
--- src/attach.c
+++ src/attach.c
@@ -63,10 +63,66 @@
6363
rc = db_column_int(&q, 0);
6464
}
6565
db_reset(&q);
6666
return rc;
6767
}
68
+
69
+/*
70
+** Given a full attachment target ID, returns its blob.rid. zTarget
71
+** must be a full wiki page name, tech-note ID, ticket ID, or forum
72
+** post hash. Returns 0 if no match is found.
73
+*/
74
+int attachment_resolve_target(const char *zTarget){
75
+ int rid = 0;
76
+ int eType = attachment_target_type(zTarget);
77
+ switch(eType){
78
+ case CFTYPE_EVENT:
79
+ rid = db_int(
80
+ 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
81
+ "WHERE tagname='event-%q'\n"
82
+ "AND x.tagtype>0\n"
83
+ "AND x.tagid=t.tagid\n"
84
+ "AND x.rid=b.rid\n"
85
+ "ORDER BY x.mtime DESC",
86
+ zTarget
87
+ );
88
+ break;
89
+ case CFTYPE_FORUM:
90
+ rid = db_int(
91
+ 0, "SELECT f.fpid FROM forumpost f, blob b\n"
92
+ "WHERE f.fpid=b.rid\n"
93
+ "AND b.uuid=%Q",
94
+ zTarget
95
+ );
96
+ break;
97
+ case CFTYPE_TICKET:
98
+ rid = db_int(
99
+ 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
100
+ "WHERE tagname='tkt-%q'\n"
101
+ "AND x.tagtype>0\n"
102
+ "AND x.tagid=t.tagid\n"
103
+ "AND x.rid=b.rid\n"
104
+ "ORDER BY x.mtime DESC",
105
+ zTarget
106
+ );
107
+ break;
108
+ case CFTYPE_WIKI:
109
+ rid = db_int(
110
+ 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
111
+ "WHERE tagname='wiki-%q'\n"
112
+ "AND x.tagtype>0\n"
113
+ "AND x.tagid=t.tagid\n"
114
+ "AND x.rid=b.rid\n"
115
+ "ORDER BY x.mtime DESC",
116
+ zTarget
117
+ );
118
+ break;
119
+ default:
120
+ break;
121
+ }
122
+ return rid;
123
+}
68124
69125
/*
70126
** For a given aritfact ID and type (from the CFTYPE_xyz enum),
71127
** returns true if the current user could hypothetically attach
72128
** something to it, else returns 0.
@@ -511,11 +567,11 @@
511567
const char *aContent;
512568
const char *zName;
513569
const char *zComment;
514570
const char *zTarget;
515571
const char *zFrom; /* Origin page - redirect here after saving */
516
- char * zTo = 0; /* Optionally redirect here after saving */
572
+ char *zTo = 0; /* Optionally redirect here after saving */
517573
char *zTargetType = 0;
518574
char *zExtraFree = 0;
519575
int szContent;
520576
int goodCaptcha = 1;
521577
int szLimit = 0;
@@ -1222,11 +1278,11 @@
12221278
const char *zHeader, /* Header to display with attachments */
12231279
const int flags /* ATTACHLIST_... flags */
12241280
){
12251281
int cnt = 0;
12261282
char szBuf[36] = {0}; /* scratchpad for attachment size value */
1227
- const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
1283
+ const char *zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
12281284
? " target=\"_blank\"" : "";
12291285
Stmt q;
12301286
db_prepare(&q,
12311287
"SELECT datetime(mtime,toLocal()), filename, user,"
12321288
" (SELECT uuid FROM blob WHERE rid=attachid), src, target, "
@@ -1435,14 +1491,14 @@
14351491
}
14361492
for(i = 2; i < g.argc; ++i){
14371493
const char *zPage = g.argv[i];
14381494
db_bind_text(&q, ":tgtname", zPage);
14391495
while(SQLITE_ROW == db_step(&q)){
1440
- const char * zTime = db_column_text(&q, 0);
1441
- const char * zSrc = db_column_text(&q, 1);
1442
- const char * zTarget = db_column_text(&q, 2);
1443
- const char * zName = db_column_text(&q, 3);
1496
+ const char *zTime = db_column_text(&q, 0);
1497
+ const char *zSrc = db_column_text(&q, 1);
1498
+ const char *zTarget = db_column_text(&q, 2);
1499
+ const char *zName = db_column_text(&q, 3);
14441500
printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName);
14451501
}
14461502
db_reset(&q);
14471503
}
14481504
db_finalize(&q);
@@ -1495,36 +1551,49 @@
14951551
case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break;
14961552
default:
14971553
goto empty_result;
14981554
}
14991555
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",
1556
+ "SELECT datetime(mtime), a.src, a.target, a.filename, a.isLatest,\n"
1557
+ " b2.size, b1.uuid, a.comment\n"
1558
+ " FROM attachment a, blob b1, blob b2\n"
1559
+ " WHERE a.target=%Q\n"
1560
+ " AND b1.rid=a.attachid\n"
1561
+ " AND b2.uuid=a.src\n"
1562
+ " AND b2.size>0\n"
1563
+ " AND (a.isLatest OR %d)\n"
1564
+ " ORDER BY a.target, a.isLatest DESC, a.mtime DESC\n",
15061565
zTgt, !bLatestOnly
15071566
);
15081567
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);
1568
+ const char *zTime = db_column_text(&q, 0);
1569
+ const char *zSrc = db_column_text(&q, 1);
1570
+ const char *zTarget = db_column_text(&q, 2);
1571
+ const char *zName = db_column_text(&q, 3);
15131572
const int isLatest = db_column_int(&q, 4);
1514
- const char * zUuid = db_column_text(&q, 5);
1573
+ const int sz = db_column_int(&q, 5);
1574
+ const char *zUuid = db_column_text(&q, 6);
1575
+ const char *zComment = db_column_text(&q, 7);
15151576
if(!i++){
15161577
blob_append_char(pOut, '[');
15171578
}else{
15181579
blob_append_char(pOut, ',');
15191580
}
15201581
blob_appendf(
15211582
pOut,
15221583
"{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
1523
- "\"filename\": %!j, \"mtime\": %!j, \"isLatest\": %s}",
1584
+ "\"filename\": %!j, \"size\":%d, \"mtime\": %!j, "
1585
+ "\"isLatest\": %s,\"comment\": ",
15241586
zUuid, zSrc, zTarget,
1525
- zName, zTime, isLatest ? "true" : "false");
1587
+ zName, sz, zTime, isLatest ? "true" : "false"
1588
+ );
1589
+ if( zComment && zComment[0] ){
1590
+ blob_appendf(pOut, "%!j", zComment);
1591
+ }else{
1592
+ blob_append_literal(pOut, "null");
1593
+ }
1594
+ blob_append_char(pOut, '}');
15261595
}
15271596
fossil_free(zToFree);
15281597
db_finalize(&q);
15291598
if(!i){
15301599
empty_result:
@@ -1536,5 +1605,47 @@
15361605
}else{
15371606
blob_append_char(pOut, ']');
15381607
}
15391608
return i;
15401609
}
1610
+
1611
+/*
1612
+** COMMAND: test-attachments-to-json
1613
+**
1614
+** Usage: %fossil test-attachments-to-json FULL_TARGET_ID
1615
+**
1616
+** Options:
1617
+** --old List all versions of attachments. Default is to
1618
+** list only the latest.
1619
+**
1620
+** Emits a JSON array of attachments for the given attachment target.
1621
+** The given ID must be a full wiki page name, ticket hash, tech-note
1622
+** hash, or forum post hash. It does not accept partial prefixes.
1623
+*/
1624
+void test_attachments_to_json_cmd(void){
1625
+ Manifest *pManifest;
1626
+ Blob b = BLOB_INITIALIZER;
1627
+ int bLatestOnly = find_option("old",0,0)==0;
1628
+ int emptyPolicy = 1;
1629
+ int rid;
1630
+ const char *zTarget;
1631
+ verify_all_options();
1632
+ db_find_and_open_repository(0, 0);
1633
+ if( g.argc<3 ){
1634
+ usage("test-attachments-to-json TARGET_ID");
1635
+ return;
1636
+ }
1637
+ zTarget = g.argv[2];
1638
+ rid = attachment_resolve_target(zTarget);
1639
+ if( 0==rid ){
1640
+ fossil_fatal("Cannot resolve ID.");
1641
+ }
1642
+ pManifest = manifest_get(rid, CFTYPE_ANY, NULL);
1643
+ attachments_to_json(pManifest, &b, bLatestOnly, emptyPolicy);
1644
+ if( b.nUsed ){
1645
+ char *zPretty = db_text(0,"SELECT json_pretty(%B)", &b);
1646
+ fossil_print("%s\n", zPretty);
1647
+ fossil_free(zPretty);
1648
+ }
1649
+ blob_reset(&b);
1650
+ manifest_destroy(pManifest);
1651
+}
15411652
--- src/attach.c
+++ src/attach.c
@@ -63,10 +63,66 @@
63 rc = db_column_int(&q, 0);
64 }
65 db_reset(&q);
66 return rc;
67 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
69 /*
70 ** For a given aritfact ID and type (from the CFTYPE_xyz enum),
71 ** returns true if the current user could hypothetically attach
72 ** something to it, else returns 0.
@@ -511,11 +567,11 @@
511 const char *aContent;
512 const char *zName;
513 const char *zComment;
514 const char *zTarget;
515 const char *zFrom; /* Origin page - redirect here after saving */
516 char * zTo = 0; /* Optionally redirect here after saving */
517 char *zTargetType = 0;
518 char *zExtraFree = 0;
519 int szContent;
520 int goodCaptcha = 1;
521 int szLimit = 0;
@@ -1222,11 +1278,11 @@
1222 const char *zHeader, /* Header to display with attachments */
1223 const int flags /* ATTACHLIST_... flags */
1224 ){
1225 int cnt = 0;
1226 char szBuf[36] = {0}; /* scratchpad for attachment size value */
1227 const char * zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
1228 ? " target=\"_blank\"" : "";
1229 Stmt q;
1230 db_prepare(&q,
1231 "SELECT datetime(mtime,toLocal()), filename, user,"
1232 " (SELECT uuid FROM blob WHERE rid=attachid), src, target, "
@@ -1435,14 +1491,14 @@
1435 }
1436 for(i = 2; i < g.argc; ++i){
1437 const char *zPage = g.argv[i];
1438 db_bind_text(&q, ":tgtname", zPage);
1439 while(SQLITE_ROW == db_step(&q)){
1440 const char * zTime = db_column_text(&q, 0);
1441 const char * zSrc = db_column_text(&q, 1);
1442 const char * zTarget = db_column_text(&q, 2);
1443 const char * zName = db_column_text(&q, 3);
1444 printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName);
1445 }
1446 db_reset(&q);
1447 }
1448 db_finalize(&q);
@@ -1495,36 +1551,49 @@
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:
@@ -1536,5 +1605,47 @@
1536 }else{
1537 blob_append_char(pOut, ']');
1538 }
1539 return i;
1540 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1541
--- src/attach.c
+++ src/attach.c
@@ -63,10 +63,66 @@
63 rc = db_column_int(&q, 0);
64 }
65 db_reset(&q);
66 return rc;
67 }
68
69 /*
70 ** Given a full attachment target ID, returns its blob.rid. zTarget
71 ** must be a full wiki page name, tech-note ID, ticket ID, or forum
72 ** post hash. Returns 0 if no match is found.
73 */
74 int attachment_resolve_target(const char *zTarget){
75 int rid = 0;
76 int eType = attachment_target_type(zTarget);
77 switch(eType){
78 case CFTYPE_EVENT:
79 rid = db_int(
80 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
81 "WHERE tagname='event-%q'\n"
82 "AND x.tagtype>0\n"
83 "AND x.tagid=t.tagid\n"
84 "AND x.rid=b.rid\n"
85 "ORDER BY x.mtime DESC",
86 zTarget
87 );
88 break;
89 case CFTYPE_FORUM:
90 rid = db_int(
91 0, "SELECT f.fpid FROM forumpost f, blob b\n"
92 "WHERE f.fpid=b.rid\n"
93 "AND b.uuid=%Q",
94 zTarget
95 );
96 break;
97 case CFTYPE_TICKET:
98 rid = db_int(
99 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
100 "WHERE tagname='tkt-%q'\n"
101 "AND x.tagtype>0\n"
102 "AND x.tagid=t.tagid\n"
103 "AND x.rid=b.rid\n"
104 "ORDER BY x.mtime DESC",
105 zTarget
106 );
107 break;
108 case CFTYPE_WIKI:
109 rid = db_int(
110 0, "SELECT b.rid FROM blob b, tag t, tagxref x\n"
111 "WHERE tagname='wiki-%q'\n"
112 "AND x.tagtype>0\n"
113 "AND x.tagid=t.tagid\n"
114 "AND x.rid=b.rid\n"
115 "ORDER BY x.mtime DESC",
116 zTarget
117 );
118 break;
119 default:
120 break;
121 }
122 return rid;
123 }
124
125 /*
126 ** For a given aritfact ID and type (from the CFTYPE_xyz enum),
127 ** returns true if the current user could hypothetically attach
128 ** something to it, else returns 0.
@@ -511,11 +567,11 @@
567 const char *aContent;
568 const char *zName;
569 const char *zComment;
570 const char *zTarget;
571 const char *zFrom; /* Origin page - redirect here after saving */
572 char *zTo = 0; /* Optionally redirect here after saving */
573 char *zTargetType = 0;
574 char *zExtraFree = 0;
575 int szContent;
576 int goodCaptcha = 1;
577 int szLimit = 0;
@@ -1222,11 +1278,11 @@
1278 const char *zHeader, /* Header to display with attachments */
1279 const int flags /* ATTACHLIST_... flags */
1280 ){
1281 int cnt = 0;
1282 char szBuf[36] = {0}; /* scratchpad for attachment size value */
1283 const char *zLinkTgt = (ATTACHLIST_TARGET_BLANK & flags)
1284 ? " target=\"_blank\"" : "";
1285 Stmt q;
1286 db_prepare(&q,
1287 "SELECT datetime(mtime,toLocal()), filename, user,"
1288 " (SELECT uuid FROM blob WHERE rid=attachid), src, target, "
@@ -1435,14 +1491,14 @@
1491 }
1492 for(i = 2; i < g.argc; ++i){
1493 const char *zPage = g.argv[i];
1494 db_bind_text(&q, ":tgtname", zPage);
1495 while(SQLITE_ROW == db_step(&q)){
1496 const char *zTime = db_column_text(&q, 0);
1497 const char *zSrc = db_column_text(&q, 1);
1498 const char *zTarget = db_column_text(&q, 2);
1499 const char *zName = db_column_text(&q, 3);
1500 printf("%-20s %s %.12s %s\n", zTarget, zTime, zSrc, zName);
1501 }
1502 db_reset(&q);
1503 }
1504 db_finalize(&q);
@@ -1495,36 +1551,49 @@
1551 case CFTYPE_TICKET: zTgt = pManifest->zTicketUuid; break;
1552 default:
1553 goto empty_result;
1554 }
1555 db_prepare(&q,
1556 "SELECT datetime(mtime), a.src, a.target, a.filename, a.isLatest,\n"
1557 " b2.size, b1.uuid, a.comment\n"
1558 " FROM attachment a, blob b1, blob b2\n"
1559 " WHERE a.target=%Q\n"
1560 " AND b1.rid=a.attachid\n"
1561 " AND b2.uuid=a.src\n"
1562 " AND b2.size>0\n"
1563 " AND (a.isLatest OR %d)\n"
1564 " ORDER BY a.target, a.isLatest DESC, a.mtime DESC\n",
1565 zTgt, !bLatestOnly
1566 );
1567 while(SQLITE_ROW == db_step(&q)){
1568 const char *zTime = db_column_text(&q, 0);
1569 const char *zSrc = db_column_text(&q, 1);
1570 const char *zTarget = db_column_text(&q, 2);
1571 const char *zName = db_column_text(&q, 3);
1572 const int isLatest = db_column_int(&q, 4);
1573 const int sz = db_column_int(&q, 5);
1574 const char *zUuid = db_column_text(&q, 6);
1575 const char *zComment = db_column_text(&q, 7);
1576 if(!i++){
1577 blob_append_char(pOut, '[');
1578 }else{
1579 blob_append_char(pOut, ',');
1580 }
1581 blob_appendf(
1582 pOut,
1583 "{\"uuid\": %!j, \"src\": %!j, \"target\": %!j, "
1584 "\"filename\": %!j, \"size\":%d, \"mtime\": %!j, "
1585 "\"isLatest\": %s,\"comment\": ",
1586 zUuid, zSrc, zTarget,
1587 zName, sz, zTime, isLatest ? "true" : "false"
1588 );
1589 if( zComment && zComment[0] ){
1590 blob_appendf(pOut, "%!j", zComment);
1591 }else{
1592 blob_append_literal(pOut, "null");
1593 }
1594 blob_append_char(pOut, '}');
1595 }
1596 fossil_free(zToFree);
1597 db_finalize(&q);
1598 if(!i){
1599 empty_result:
@@ -1536,5 +1605,47 @@
1605 }else{
1606 blob_append_char(pOut, ']');
1607 }
1608 return i;
1609 }
1610
1611 /*
1612 ** COMMAND: test-attachments-to-json
1613 **
1614 ** Usage: %fossil test-attachments-to-json FULL_TARGET_ID
1615 **
1616 ** Options:
1617 ** --old List all versions of attachments. Default is to
1618 ** list only the latest.
1619 **
1620 ** Emits a JSON array of attachments for the given attachment target.
1621 ** The given ID must be a full wiki page name, ticket hash, tech-note
1622 ** hash, or forum post hash. It does not accept partial prefixes.
1623 */
1624 void test_attachments_to_json_cmd(void){
1625 Manifest *pManifest;
1626 Blob b = BLOB_INITIALIZER;
1627 int bLatestOnly = find_option("old",0,0)==0;
1628 int emptyPolicy = 1;
1629 int rid;
1630 const char *zTarget;
1631 verify_all_options();
1632 db_find_and_open_repository(0, 0);
1633 if( g.argc<3 ){
1634 usage("test-attachments-to-json TARGET_ID");
1635 return;
1636 }
1637 zTarget = g.argv[2];
1638 rid = attachment_resolve_target(zTarget);
1639 if( 0==rid ){
1640 fossil_fatal("Cannot resolve ID.");
1641 }
1642 pManifest = manifest_get(rid, CFTYPE_ANY, NULL);
1643 attachments_to_json(pManifest, &b, bLatestOnly, emptyPolicy);
1644 if( b.nUsed ){
1645 char *zPretty = db_text(0,"SELECT json_pretty(%B)", &b);
1646 fossil_print("%s\n", zPretty);
1647 fossil_free(zPretty);
1648 }
1649 blob_reset(&b);
1650 manifest_destroy(pManifest);
1651 }
1652

Keyboard Shortcuts

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