Fossil SCM

Simplified tarball basenames. The download file includes sufficient information to reconstruct the check-in hash, so the check-in hash does not need to be supplied separately, resulting in shorter URLs.

drh 2025-10-18 20:57 timeline-enhance-2025
Commit df28ba6539589a0091034913ce7975284b349b1141f75b2a6f675e74bf8ef57e
2 files changed +3 -3 +71 -12
+3 -3
--- src/info.c
+++ src/info.c
@@ -998,14 +998,14 @@
998998
@ <tr><th>Downloads:</th><td>
999999
if( robot_would_be_restricted("download") ){
10001000
@ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
10011001
}else{
10021002
char *zBase = archive_base_name(rid);
1003
- @ %z(href("%R/tarball/%S/%s.tar.gz",zUuid,zBase))Tarball</a>
1004
- @ | %z(href("%R/zip/%S/%s.zip",zUuid,zBase))ZIP archive</a>
1003
+ @ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a>
1004
+ @ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a>
10051005
if( g.zLogin!=0 ){
1006
- @ | %z(href("%R/sqlar/%S/%s.sqlar",zUuid,zBase))\
1006
+ @ | %z(href("%R/sqlar/%s.sqlar",zBase))\
10071007
@ SQL archive</a></td></tr>
10081008
}
10091009
fossil_free(zBase);
10101010
}
10111011
}
10121012
--- src/info.c
+++ src/info.c
@@ -998,14 +998,14 @@
998 @ <tr><th>Downloads:</th><td>
999 if( robot_would_be_restricted("download") ){
1000 @ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
1001 }else{
1002 char *zBase = archive_base_name(rid);
1003 @ %z(href("%R/tarball/%S/%s.tar.gz",zUuid,zBase))Tarball</a>
1004 @ | %z(href("%R/zip/%S/%s.zip",zUuid,zBase))ZIP archive</a>
1005 if( g.zLogin!=0 ){
1006 @ | %z(href("%R/sqlar/%S/%s.sqlar",zUuid,zBase))\
1007 @ SQL archive</a></td></tr>
1008 }
1009 fossil_free(zBase);
1010 }
1011 }
1012
--- src/info.c
+++ src/info.c
@@ -998,14 +998,14 @@
998 @ <tr><th>Downloads:</th><td>
999 if( robot_would_be_restricted("download") ){
1000 @ See separate %z(href("%R/rchvdwnld/%!S",zUuid))download page</a>
1001 }else{
1002 char *zBase = archive_base_name(rid);
1003 @ %z(href("%R/tarball/%s.tar.gz",zBase))Tarball</a>
1004 @ | %z(href("%R/zip/%s.zip",zBase))ZIP archive</a>
1005 if( g.zLogin!=0 ){
1006 @ | %z(href("%R/sqlar/%s.sqlar",zBase))\
1007 @ SQL archive</a></td></tr>
1008 }
1009 fossil_free(zBase);
1010 }
1011 }
1012
+71 -12
--- src/tar.c
+++ src/tar.c
@@ -704,27 +704,76 @@
704704
if( zOut ){
705705
blob_write_to_file(&tarball, zOut);
706706
blob_reset(&tarball);
707707
}
708708
}
709
+
710
+/*
711
+** This is a helper routine for tar_uuid_from_name(). It handles
712
+** the case where *pzName contains no "/" character. Check for
713
+** format (3). Return the hash if the name matches format (3),
714
+** or return NULL if it does not.
715
+*/
716
+static char *format_three_parser(const char *zName){
717
+ int iDot = 0; /* Index in zName[] of the first '.' */
718
+ int iDash1 = 0; /* Index in zName[] of the '-' before the timestamp */
719
+ int iDash2 = 0; /* Index in zName[] of the '-' between timestamp and hash */
720
+ int nHash; /* Size of the hash */
721
+ char *zHash; /* A copy of the hash value */
722
+ char *zDate; /* Copy of the timestamp */
723
+ char *zUuid; /* Final result */
724
+ int i; /* Loop query */
725
+ Stmt q; /* Query to verify that hash and timestamp agree */
726
+
727
+ for(i=0; zName[i]; i++){
728
+ char c = zName[i];
729
+ if( c=='.' ){ iDot = i; break; }
730
+ if( c=='-' ){ iDash1 = iDash2; iDash2 = i; }
731
+ if( !fossil_isalnum(c) && c!='_' && c!='-' ){ break; }
732
+ }
733
+ if( iDot==0 ) return 0;
734
+ if( iDash1==0 ) return 0;
735
+ nHash = iDot - iDash2 - 1;
736
+ if( nHash<8 ) return 0; /* HASH value too short */
737
+ if( (iDash2 - iDash1)!=15 ) return 0; /* Wrong timestamp size */
738
+ zHash = fossil_strndup(&zName[iDash2+1], nHash);
739
+ zDate = fossil_strndup(&zName[iDash1+1], 14);
740
+ db_prepare(&q,
741
+ "SELECT blob.uuid"
742
+ " FROM blob JOIN event ON event.objid=blob.rid"
743
+ " WHERE blob.uuid GLOB '%q*'"
744
+ " AND strftime('%%Y%%m%%d%%H%%M%%S',event.mtime)='%q'",
745
+ zHash, zDate
746
+ );
747
+ fossil_free(zHash);
748
+ fossil_free(zDate);
749
+ if( db_step(&q)==SQLITE_ROW ){
750
+ zUuid = fossil_strdup(db_column_text(&q,0));
751
+ }else{
752
+ zUuid = 0;
753
+ }
754
+ db_finalize(&q);
755
+ return zUuid;
756
+}
709757
710758
/*
711759
** Check to see if the input string is of one of the following
712760
** two the forms:
713761
**
714762
** check-in-name/filename.ext (1)
715
-** tag-name/check-in-name/filename.txt (2)
763
+** tag-name/check-in-name/filename.ext (2)
764
+** project-datetime-hash.ext (3)
716765
**
717766
** In other words, check to see if the input string contains either
718767
** a check-in name or a tag-name and a check-in name separated by
719
-** a slash. There must be either 1 or 2 "/" characters. In the
768
+** a slash. There must be between 0 or 2 "/" characters. In the
720769
** second form, tag-name must be an individual tag (not a branch-tag)
721770
** that is found on the check-in identified by the check-in-name.
722771
**
723772
** If the condition is true, then:
724773
**
725
-** * Make *pzName point to the fielname suffix only
774
+** * Make *pzName point to the filename suffix only
726775
** * return a copy of the check-in name in memory from mprintf().
727776
**
728777
** If the condition is false, leave *pzName unchanged and return either
729778
** NULL or an empty string. Normally NULL is returned, however an
730779
** empty string is returned for format (2) if check-in-name does not
@@ -736,10 +785,19 @@
736785
**
737786
** Such URLs will pass through most anti-robot filters because of the
738787
** "/tarball/release" prefix will match the suggested "robot-exception"
739788
** pattern and can still refer to an historic release rather than just
740789
** the most recent release.
790
+**
791
+** Format (3) is designed to allow URLs like this:
792
+**
793
+** /tarball/fossil-20251018193920-d6c9aee97df.tar.gz
794
+**
795
+** In other words, filename itself contains sufficient information to
796
+** uniquely identify the check-in, including a timestamp of the form
797
+** YYYYMMDDHHMMSS and a prefix of the check-in hash. The timestamp
798
+** and hash must immediately preceed the first "." in the name.
741799
*/
742800
char *tar_uuid_from_name(char **pzName){
743801
char *zName = *pzName; /* Original input */
744802
int n1 = 0; /* Bytes in first prefix (tag-name) */
745803
int n2 = 0; /* Bytes in second prefix (check-in-name) */
@@ -755,11 +813,12 @@
755813
return 0; /* More than two "/" characters seen */
756814
}
757815
}
758816
}
759817
if( n1==0 ){
760
- return 0; /* No prefix of any kind */
818
+ /* Check for format (3) */
819
+ return format_three_parser(*pzName);
761820
}
762821
if( zName[n+1]==0 ){
763822
return 0; /* No filename suffix */
764823
}
765824
if( n2==0 ){
@@ -1002,13 +1061,13 @@
10021061
@ branch:&nbsp;\
10031062
@ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
10041063
}
10051064
}
10061065
zNm = archive_base_name(rid);
1007
- @ %z(href("%R/tarball/%!S/%s.tar.gz",zUuid,zNm))\
1066
+ @ %z(href("%R/tarball/%s.tar.gz",zNm))\
10081067
@ <button>Tarball</button></a>
1009
- @ %z(href("%R/zip/%!S/%s.zip",zUuid,zNm))\
1068
+ @ %z(href("%R/zip/%s.zip",zNm))\
10101069
@ <button>ZIP&nbsp;Archive</button></a>
10111070
fossil_free(zBrName);
10121071
fossil_free(zNm);
10131072
}
10141073
}
@@ -1176,27 +1235,27 @@
11761235
@ %z(href("%R/info/%!S",zUuid))%S(zUuid)</a></div>
11771236
@ <div class="accordion_panel">
11781237
@ <table class="label-value">
11791238
@ <tr>
11801239
@ <th>Tarball:</th>
1181
- @ <td>%z(href("%R/tarball/%!S/%s.tar.gz",zUuid,zBase))\
1182
- @ %s(g.zBaseURL)/tarball/%!S(zUuid)/%s(zBase).tar.gz</a></td>
1240
+ @ <td>%z(href("%R/tarball/%s.tar.gz",zBase))\
1241
+ @ %s(g.zBaseURL)/tarball/%s(zBase).tar.gz</a></td>
11831242
@ </tr>
11841243
@
11851244
@ <tr>
11861245
@ <th>ZIP:</th>
1187
- @ <td>%z(href("%R/zip/%!S/%s.zip",zUuid,zBase))\
1188
- @ %s(g.zBaseURL)/zip/%!S(zUuid)/%s(zBase).zip</a></td>
1246
+ @ <td>%z(href("%R/zip/%s.zip",zBase))\
1247
+ @ %s(g.zBaseURL)/zip/%s(zBase).zip</a></td>
11891248
@ </tr>
11901249
@
11911250
@ <tr>
11921251
@ <th>SQLAR:</th>
1193
- @ <td>%z(href("%R/sqlar/%!S/%s.sqlar",zUuid,zBase))\
1194
- @ %s(g.zBaseURL)/sqlar/%!S(zUuid)/%s(zBase).sqlar</a></td>
1252
+ @ <td>%z(href("%R/sqlar/%s.sqlar",zBase))\
1253
+ @ %s(g.zBaseURL)/sqlar/%s(zBase).sqlar</a></td>
11951254
@ </tr>
11961255
@ </table></div>
11971256
fossil_free(zBase);
11981257
@ <div class="section accordion">Context</div><div class="accordion_panel">
11991258
render_checkin_context(rid, 0, 0, 0);
12001259
@ </div>
12011260
style_finish_page();
12021261
}
12031262
--- src/tar.c
+++ src/tar.c
@@ -704,27 +704,76 @@
704 if( zOut ){
705 blob_write_to_file(&tarball, zOut);
706 blob_reset(&tarball);
707 }
708 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
709
710 /*
711 ** Check to see if the input string is of one of the following
712 ** two the forms:
713 **
714 ** check-in-name/filename.ext (1)
715 ** tag-name/check-in-name/filename.txt (2)
 
716 **
717 ** In other words, check to see if the input string contains either
718 ** a check-in name or a tag-name and a check-in name separated by
719 ** a slash. There must be either 1 or 2 "/" characters. In the
720 ** second form, tag-name must be an individual tag (not a branch-tag)
721 ** that is found on the check-in identified by the check-in-name.
722 **
723 ** If the condition is true, then:
724 **
725 ** * Make *pzName point to the fielname suffix only
726 ** * return a copy of the check-in name in memory from mprintf().
727 **
728 ** If the condition is false, leave *pzName unchanged and return either
729 ** NULL or an empty string. Normally NULL is returned, however an
730 ** empty string is returned for format (2) if check-in-name does not
@@ -736,10 +785,19 @@
736 **
737 ** Such URLs will pass through most anti-robot filters because of the
738 ** "/tarball/release" prefix will match the suggested "robot-exception"
739 ** pattern and can still refer to an historic release rather than just
740 ** the most recent release.
 
 
 
 
 
 
 
 
 
741 */
742 char *tar_uuid_from_name(char **pzName){
743 char *zName = *pzName; /* Original input */
744 int n1 = 0; /* Bytes in first prefix (tag-name) */
745 int n2 = 0; /* Bytes in second prefix (check-in-name) */
@@ -755,11 +813,12 @@
755 return 0; /* More than two "/" characters seen */
756 }
757 }
758 }
759 if( n1==0 ){
760 return 0; /* No prefix of any kind */
 
761 }
762 if( zName[n+1]==0 ){
763 return 0; /* No filename suffix */
764 }
765 if( n2==0 ){
@@ -1002,13 +1061,13 @@
1002 @ branch:&nbsp;\
1003 @ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
1004 }
1005 }
1006 zNm = archive_base_name(rid);
1007 @ %z(href("%R/tarball/%!S/%s.tar.gz",zUuid,zNm))\
1008 @ <button>Tarball</button></a>
1009 @ %z(href("%R/zip/%!S/%s.zip",zUuid,zNm))\
1010 @ <button>ZIP&nbsp;Archive</button></a>
1011 fossil_free(zBrName);
1012 fossil_free(zNm);
1013 }
1014 }
@@ -1176,27 +1235,27 @@
1176 @ %z(href("%R/info/%!S",zUuid))%S(zUuid)</a></div>
1177 @ <div class="accordion_panel">
1178 @ <table class="label-value">
1179 @ <tr>
1180 @ <th>Tarball:</th>
1181 @ <td>%z(href("%R/tarball/%!S/%s.tar.gz",zUuid,zBase))\
1182 @ %s(g.zBaseURL)/tarball/%!S(zUuid)/%s(zBase).tar.gz</a></td>
1183 @ </tr>
1184 @
1185 @ <tr>
1186 @ <th>ZIP:</th>
1187 @ <td>%z(href("%R/zip/%!S/%s.zip",zUuid,zBase))\
1188 @ %s(g.zBaseURL)/zip/%!S(zUuid)/%s(zBase).zip</a></td>
1189 @ </tr>
1190 @
1191 @ <tr>
1192 @ <th>SQLAR:</th>
1193 @ <td>%z(href("%R/sqlar/%!S/%s.sqlar",zUuid,zBase))\
1194 @ %s(g.zBaseURL)/sqlar/%!S(zUuid)/%s(zBase).sqlar</a></td>
1195 @ </tr>
1196 @ </table></div>
1197 fossil_free(zBase);
1198 @ <div class="section accordion">Context</div><div class="accordion_panel">
1199 render_checkin_context(rid, 0, 0, 0);
1200 @ </div>
1201 style_finish_page();
1202 }
1203
--- src/tar.c
+++ src/tar.c
@@ -704,27 +704,76 @@
704 if( zOut ){
705 blob_write_to_file(&tarball, zOut);
706 blob_reset(&tarball);
707 }
708 }
709
710 /*
711 ** This is a helper routine for tar_uuid_from_name(). It handles
712 ** the case where *pzName contains no "/" character. Check for
713 ** format (3). Return the hash if the name matches format (3),
714 ** or return NULL if it does not.
715 */
716 static char *format_three_parser(const char *zName){
717 int iDot = 0; /* Index in zName[] of the first '.' */
718 int iDash1 = 0; /* Index in zName[] of the '-' before the timestamp */
719 int iDash2 = 0; /* Index in zName[] of the '-' between timestamp and hash */
720 int nHash; /* Size of the hash */
721 char *zHash; /* A copy of the hash value */
722 char *zDate; /* Copy of the timestamp */
723 char *zUuid; /* Final result */
724 int i; /* Loop query */
725 Stmt q; /* Query to verify that hash and timestamp agree */
726
727 for(i=0; zName[i]; i++){
728 char c = zName[i];
729 if( c=='.' ){ iDot = i; break; }
730 if( c=='-' ){ iDash1 = iDash2; iDash2 = i; }
731 if( !fossil_isalnum(c) && c!='_' && c!='-' ){ break; }
732 }
733 if( iDot==0 ) return 0;
734 if( iDash1==0 ) return 0;
735 nHash = iDot - iDash2 - 1;
736 if( nHash<8 ) return 0; /* HASH value too short */
737 if( (iDash2 - iDash1)!=15 ) return 0; /* Wrong timestamp size */
738 zHash = fossil_strndup(&zName[iDash2+1], nHash);
739 zDate = fossil_strndup(&zName[iDash1+1], 14);
740 db_prepare(&q,
741 "SELECT blob.uuid"
742 " FROM blob JOIN event ON event.objid=blob.rid"
743 " WHERE blob.uuid GLOB '%q*'"
744 " AND strftime('%%Y%%m%%d%%H%%M%%S',event.mtime)='%q'",
745 zHash, zDate
746 );
747 fossil_free(zHash);
748 fossil_free(zDate);
749 if( db_step(&q)==SQLITE_ROW ){
750 zUuid = fossil_strdup(db_column_text(&q,0));
751 }else{
752 zUuid = 0;
753 }
754 db_finalize(&q);
755 return zUuid;
756 }
757
758 /*
759 ** Check to see if the input string is of one of the following
760 ** two the forms:
761 **
762 ** check-in-name/filename.ext (1)
763 ** tag-name/check-in-name/filename.ext (2)
764 ** project-datetime-hash.ext (3)
765 **
766 ** In other words, check to see if the input string contains either
767 ** a check-in name or a tag-name and a check-in name separated by
768 ** a slash. There must be between 0 or 2 "/" characters. In the
769 ** second form, tag-name must be an individual tag (not a branch-tag)
770 ** that is found on the check-in identified by the check-in-name.
771 **
772 ** If the condition is true, then:
773 **
774 ** * Make *pzName point to the filename suffix only
775 ** * return a copy of the check-in name in memory from mprintf().
776 **
777 ** If the condition is false, leave *pzName unchanged and return either
778 ** NULL or an empty string. Normally NULL is returned, however an
779 ** empty string is returned for format (2) if check-in-name does not
@@ -736,10 +785,19 @@
785 **
786 ** Such URLs will pass through most anti-robot filters because of the
787 ** "/tarball/release" prefix will match the suggested "robot-exception"
788 ** pattern and can still refer to an historic release rather than just
789 ** the most recent release.
790 **
791 ** Format (3) is designed to allow URLs like this:
792 **
793 ** /tarball/fossil-20251018193920-d6c9aee97df.tar.gz
794 **
795 ** In other words, filename itself contains sufficient information to
796 ** uniquely identify the check-in, including a timestamp of the form
797 ** YYYYMMDDHHMMSS and a prefix of the check-in hash. The timestamp
798 ** and hash must immediately preceed the first "." in the name.
799 */
800 char *tar_uuid_from_name(char **pzName){
801 char *zName = *pzName; /* Original input */
802 int n1 = 0; /* Bytes in first prefix (tag-name) */
803 int n2 = 0; /* Bytes in second prefix (check-in-name) */
@@ -755,11 +813,12 @@
813 return 0; /* More than two "/" characters seen */
814 }
815 }
816 }
817 if( n1==0 ){
818 /* Check for format (3) */
819 return format_three_parser(*pzName);
820 }
821 if( zName[n+1]==0 ){
822 return 0; /* No filename suffix */
823 }
824 if( n2==0 ){
@@ -1002,13 +1061,13 @@
1061 @ branch:&nbsp;\
1062 @ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
1063 }
1064 }
1065 zNm = archive_base_name(rid);
1066 @ %z(href("%R/tarball/%s.tar.gz",zNm))\
1067 @ <button>Tarball</button></a>
1068 @ %z(href("%R/zip/%s.zip",zNm))\
1069 @ <button>ZIP&nbsp;Archive</button></a>
1070 fossil_free(zBrName);
1071 fossil_free(zNm);
1072 }
1073 }
@@ -1176,27 +1235,27 @@
1235 @ %z(href("%R/info/%!S",zUuid))%S(zUuid)</a></div>
1236 @ <div class="accordion_panel">
1237 @ <table class="label-value">
1238 @ <tr>
1239 @ <th>Tarball:</th>
1240 @ <td>%z(href("%R/tarball/%s.tar.gz",zBase))\
1241 @ %s(g.zBaseURL)/tarball/%s(zBase).tar.gz</a></td>
1242 @ </tr>
1243 @
1244 @ <tr>
1245 @ <th>ZIP:</th>
1246 @ <td>%z(href("%R/zip/%s.zip",zBase))\
1247 @ %s(g.zBaseURL)/zip/%s(zBase).zip</a></td>
1248 @ </tr>
1249 @
1250 @ <tr>
1251 @ <th>SQLAR:</th>
1252 @ <td>%z(href("%R/sqlar/%s.sqlar",zBase))\
1253 @ %s(g.zBaseURL)/sqlar/%s(zBase).sqlar</a></td>
1254 @ </tr>
1255 @ </table></div>
1256 fossil_free(zBase);
1257 @ <div class="section accordion">Context</div><div class="accordion_panel">
1258 render_checkin_context(rid, 0, 0, 0);
1259 @ </div>
1260 style_finish_page();
1261 }
1262

Keyboard Shortcuts

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