Fossil SCM

fossil-scm / src / tar.c
Blame History Raw 1317 lines
1
/*
2
** Copyright (c) 2011 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This file contains code used to generate tarballs.
19
*/
20
#include "config.h"
21
#include <assert.h>
22
#include <zlib.h>
23
#include "tar.h"
24
25
/*
26
** State information for the tarball builder.
27
*/
28
static struct tarball_t {
29
unsigned char *aHdr; /* Space for building headers */
30
char *zSpaces; /* Spaces for padding */
31
char *zPrevDir; /* Name of directory for previous entry */
32
int nPrevDirAlloc; /* size of zPrevDir */
33
Blob pax; /* PAX data */
34
} tball;
35
36
/*
37
** Convert a string so that it contains only lower-case ASCII, digits,
38
** "_" and "-". Changes are made in-place.
39
*/
40
static void sanitize_name(char *zName){
41
int i;
42
char c;
43
if( zName==0 ) return;
44
for(i=0; (c = zName[i])!=0; i++){
45
if( fossil_isupper(c) ){
46
zName[i] = fossil_tolower(c);
47
}else if( !fossil_isalnum(c) && c!='_' && c!='-' ){
48
if( c<=0x7f ){
49
zName[i] = '_';
50
}else{
51
/* 123456789 123456789 123456 */
52
zName[i] = "abcdefghijklmnopqrstuvwxyz"[(unsigned)c%26];
53
}
54
}
55
}
56
}
57
58
/*
59
** Compute a sensible base-name for an archive file (tarball, ZIP, or SQLAR)
60
** based on the rid of the check-in contained in that file.
61
**
62
** PROJECTNAME-DATETIME-HASHPREFIX
63
**
64
** So that the name will be safe to use as a URL or a filename on any system,
65
** the name is only allowed to contain lower-case ASCII alphabetics,
66
** digits, '_' and '-'. Upper-case ASCII is converted to lower-case. All
67
** other bytes are mapped into a lower-case alphabetic.
68
**
69
** The value returned is obtained from mprintf() or fossil_strdup() and should
70
** be released by the caller using fossil_free().
71
*/
72
char *archive_base_name(int rid){
73
char *zPrefix;
74
char *zName;
75
zPrefix = db_get("short-project-name",0);
76
if( zPrefix==0 || zPrefix[0]==0 ){
77
zPrefix = db_get("project-name","unnamed");
78
}
79
zName = db_text(0,
80
"SELECT %Q||"
81
" strftime('-%%Y%%m%%d%%H%%M%%S-',event.mtime)||"
82
" substr(blob.uuid,1,10)"
83
" FROM blob, event"
84
" WHERE blob.rid=%d"
85
" AND event.objid=%d",
86
zPrefix, rid, rid);
87
fossil_free(zPrefix);
88
sanitize_name(zName);
89
return zName;
90
}
91
92
/*
93
** field lengths of 'ustar' name and prefix fields.
94
*/
95
#define USTAR_NAME_LEN 100
96
#define USTAR_PREFIX_LEN 155
97
98
99
/*
100
** Begin the process of generating a tarball.
101
**
102
** Initialize the GZIP compressor and the table of directory names.
103
*/
104
static void tar_begin(sqlite3_int64 mTime){
105
assert( tball.aHdr==0 );
106
tball.aHdr = fossil_malloc(512+512);
107
memset(tball.aHdr, 0, 512+512);
108
tball.zSpaces = (char*)&tball.aHdr[512];
109
/* zPrevDir init */
110
tball.zPrevDir = NULL;
111
tball.nPrevDirAlloc = 0;
112
/* scratch buffer init */
113
blob_zero(&tball.pax);
114
115
memcpy(&tball.aHdr[108], "0000000", 8); /* Owner ID */
116
memcpy(&tball.aHdr[116], "0000000", 8); /* Group ID */
117
memcpy(&tball.aHdr[257], "ustar\00000", 8); /* POSIX.1 format */
118
memcpy(&tball.aHdr[265], "nobody", 7); /* Owner name */
119
memcpy(&tball.aHdr[297], "nobody", 7); /* Group name */
120
gzip_begin(mTime);
121
db_multi_exec(
122
"CREATE TEMP TABLE dir(name UNIQUE);"
123
);
124
}
125
126
127
/*
128
** Verify that all characters in 'zName' are in the
129
** ISO646 (=ASCII) character set.
130
*/
131
static int is_iso646_name(
132
const char *zName, /* file path */
133
int nName /* path length */
134
){
135
int i;
136
for(i = 0; i < nName; i++){
137
unsigned char c = (unsigned char)zName[i];
138
if( c>0x7e ) return 0;
139
}
140
return 1;
141
}
142
143
144
/*
145
** copy string pSrc into pDst, truncating or padding with 0 if necessary
146
*/
147
static void padded_copy(
148
char *pDest,
149
int nDest,
150
const char *pSrc,
151
int nSrc
152
){
153
if(nSrc >= nDest){
154
memcpy(pDest, pSrc, nDest);
155
}else{
156
memcpy(pDest, pSrc, nSrc);
157
memset(&pDest[nSrc], 0, nDest - nSrc);
158
}
159
}
160
161
162
163
/******************************************************************************
164
**
165
** The 'tar' format has evolved over time. Initially the name was stored
166
** in a 100 byte null-terminated field 'name'. File path names were
167
** limited to 99 bytes.
168
**
169
** The Posix.1 'ustar' format added a 155 byte field 'prefix', allowing
170
** for up to 255 characters to be stored. The full file path is formed by
171
** concatenating the field 'prefix', a slash, and the field 'name'. This
172
** gives some measure of compatibility with programs that only understand
173
** the oldest format.
174
**
175
** The latest Posix extension is called the 'pax Interchange Format'.
176
** It removes all the limitations of the previous two formats by allowing
177
** the storage of arbitrary-length attributes in a separate object that looks
178
** like a file to programs that do not understand this extension. So the
179
** contents of the 'name' and 'prefix' fields should contain values that allow
180
** versions of tar that do not understand this extension to still do
181
** something useful.
182
**
183
******************************************************************************/
184
185
/*
186
** The position we use to split a file path into the 'name' and 'prefix'
187
** fields needs to meet the following criteria:
188
**
189
** - not at the beginning or end of the string
190
** - the position must contain a slash
191
** - no more than 100 characters follow the slash
192
** - no more than 155 characters precede it
193
**
194
** The routine 'find_split_pos' finds a split position. It will meet the
195
** criteria of listed above if such a position exists. If no such
196
** position exists it generates one that useful for generating the
197
** values used for backward compatibility.
198
*/
199
static int find_split_pos(
200
const char *zName, /* file path */
201
int nName /* path length */
202
){
203
int i, split = 0;
204
/* only search if the string needs splitting */
205
if(nName > USTAR_NAME_LEN){
206
for(i = 1; i+1 < nName; i++)
207
if(zName[i] == '/'){
208
split = i+1;
209
/* if the split position is within USTAR_NAME_LEN bytes from
210
* the end we can quit */
211
if(nName - split <= USTAR_NAME_LEN) break;
212
}
213
}
214
return split;
215
}
216
217
218
/*
219
** attempt to split the file name path to meet 'ustar' header
220
** criteria.
221
*/
222
static int tar_split_path(
223
const char *zName, /* path */
224
int nName, /* path length */
225
char *pName, /* name field */
226
char *pPrefix /* prefix field */
227
){
228
int split = find_split_pos(zName, nName);
229
/* check whether both pieces fit */
230
if(nName - split > USTAR_NAME_LEN || split > USTAR_PREFIX_LEN+1){
231
return 0; /* no */
232
}
233
234
/* extract name */
235
padded_copy(pName, USTAR_NAME_LEN, &zName[split], nName - split);
236
237
/* extract prefix */
238
padded_copy(pPrefix, USTAR_PREFIX_LEN, zName, (split > 0 ? split - 1 : 0));
239
240
return 1; /* success */
241
}
242
243
244
/*
245
** When using an extension header we still need to put something
246
** reasonable in the name and prefix fields. This is probably as
247
** good as it gets.
248
*/
249
static void approximate_split_path(
250
const char *zName, /* path */
251
int nName, /* path length */
252
char *pName, /* name field */
253
char *pPrefix, /* prefix field */
254
int bHeader /* is this a 'x' type tar header? */
255
){
256
int split;
257
258
/* if this is a Pax Interchange header prepend "PaxHeader/"
259
** so we can tell files apart from metadata */
260
if( bHeader ){
261
blob_reset(&tball.pax);
262
blob_appendf(&tball.pax, "PaxHeader/%*.*s", nName, nName, zName);
263
zName = blob_buffer(&tball.pax);
264
nName = blob_size(&tball.pax);
265
}
266
267
/* find the split position */
268
split = find_split_pos(zName, nName);
269
270
/* extract a name, truncate if needed */
271
padded_copy(pName, USTAR_NAME_LEN, &zName[split], nName - split);
272
273
/* extract a prefix field, truncate when needed */
274
padded_copy(pPrefix, USTAR_PREFIX_LEN, zName, (split > 0 ? split-1 : 0));
275
}
276
277
278
/*
279
** add a Pax Interchange header to the scratch buffer
280
**
281
** format: <length> <key>=<value>\n
282
** the tricky part is that each header contains its own
283
** size in decimal, counting that length.
284
*/
285
static void add_pax_header(
286
const char *zField,
287
const char *zValue,
288
int nValue
289
){
290
/* calculate length without length field */
291
int blen = strlen(zField) + nValue + 3;
292
/* calculate the length of the length field */
293
int next10 = 1;
294
int n;
295
for(n = blen; n > 0; ){
296
blen++; next10 *= 10;
297
n /= 10;
298
}
299
/* adding the length extended the length field? */
300
if(blen > next10){
301
blen++;
302
}
303
/* build the string */
304
blob_appendf(&tball.pax, "%d %s=%*.*s\n",
305
blen, zField, nValue, nValue, zValue);
306
/* this _must_ be right */
307
if((int)blob_size(&tball.pax) != blen){
308
fossil_panic("internal error: PAX tar header has bad length");
309
}
310
}
311
312
313
/*
314
** set the header type, calculate the checksum and output
315
** the header
316
*/
317
static void cksum_and_write_header(
318
char cType
319
){
320
unsigned int cksum = 0;
321
int i;
322
memset(&tball.aHdr[148], ' ', 8);
323
tball.aHdr[156] = cType;
324
for(i=0; i<512; i++) cksum += tball.aHdr[i];
325
sqlite3_snprintf(8, (char*)&tball.aHdr[148], "%07o", cksum);
326
tball.aHdr[155] = 0;
327
gzip_step((char*)tball.aHdr, 512);
328
}
329
330
331
/*
332
** Build a header for a file or directory and write that header
333
** into the growing tarball.
334
*/
335
static void tar_add_header(
336
const char *zName, /* Name of the object */
337
int nName, /* Number of characters in zName */
338
int iMode, /* Mode. 0644 or 0755 */
339
unsigned int mTime, /* File modification time */
340
int iSize, /* Size of the object in bytes */
341
char cType /* Type of object:
342
'0'==file. '2'==symlink. '5'==directory */
343
){
344
/* set mode and modification time */
345
sqlite3_snprintf(8, (char*)&tball.aHdr[100], "%07o", iMode);
346
sqlite3_snprintf(12, (char*)&tball.aHdr[136], "%011o", mTime);
347
348
/* see if we need to output a Pax Interchange Header */
349
if( !is_iso646_name(zName, nName)
350
|| !tar_split_path(zName, nName, (char*)tball.aHdr, (char*)&tball.aHdr[345])
351
){
352
int lastPage;
353
/* add a file name for interoperability with older programs */
354
approximate_split_path(zName, nName, (char*)tball.aHdr,
355
(char*)&tball.aHdr[345], 1);
356
357
/* generate the Pax Interchange path header */
358
blob_reset(&tball.pax);
359
add_pax_header("path", zName, nName);
360
361
/* set the header length, and write the header */
362
sqlite3_snprintf(12, (char*)&tball.aHdr[124], "%011o",
363
blob_size(&tball.pax));
364
cksum_and_write_header('x');
365
366
/* write the Pax Interchange data */
367
gzip_step(blob_buffer(&tball.pax), blob_size(&tball.pax));
368
lastPage = blob_size(&tball.pax) % 512;
369
if( lastPage!=0 ){
370
gzip_step(tball.zSpaces, 512 - lastPage);
371
}
372
373
/* generate an approximate path for the regular header */
374
approximate_split_path(zName, nName, (char*)tball.aHdr,
375
(char*)&tball.aHdr[345], 0);
376
}
377
/* set the size */
378
sqlite3_snprintf(12, (char*)&tball.aHdr[124], "%011o", iSize);
379
380
/* write the regular header */
381
cksum_and_write_header(cType);
382
}
383
384
385
/*
386
** Recursively add an directory entry for the given file if those
387
** directories have not previously been seen.
388
*/
389
static void tar_add_directory_of(
390
const char *zName, /* Name of directory including final "/" */
391
int nName, /* Characters in zName */
392
unsigned int mTime /* Modification time */
393
){
394
int i;
395
for(i=nName-1; i>0 && zName[i]!='/'; i--){}
396
if( i<=0 ) return;
397
if( i<tball.nPrevDirAlloc
398
&& strncmp(tball.zPrevDir, zName, i)==0
399
&& tball.zPrevDir[i]==0 ) return;
400
db_multi_exec("INSERT OR IGNORE INTO dir VALUES('%#q')", i, zName);
401
if( sqlite3_changes(g.db)==0 ) return;
402
tar_add_directory_of(zName, i-1, mTime);
403
tar_add_header(zName, i, 0755, mTime, 0, '5');
404
if( i >= tball.nPrevDirAlloc ){
405
int nsize = tball.nPrevDirAlloc * 2;
406
if(i+1 > nsize)
407
nsize = i+1;
408
tball.zPrevDir = fossil_realloc(tball.zPrevDir, nsize);
409
tball.nPrevDirAlloc = nsize;
410
}
411
memcpy(tball.zPrevDir, zName, i);
412
tball.zPrevDir[i] = 0;
413
}
414
415
416
/*
417
** Add a single file to the growing tarball.
418
*/
419
static void tar_add_file(
420
const char *zName, /* Name of the file. nul-terminated */
421
Blob *pContent, /* Content of the file */
422
int mPerm, /* 1: executable file, 2: symlink */
423
unsigned int mTime /* Last modification time of the file */
424
){
425
int nName = strlen(zName);
426
int n = blob_size(pContent);
427
int lastPage;
428
char cType = '0';
429
430
/* length check moved to tar_split_path */
431
tar_add_directory_of(zName, nName, mTime);
432
433
/*
434
* If we have a symlink, write its destination path (which is stored in
435
* pContent) into header, and set content length to 0 to avoid storing path
436
* as file content in the next step. Since 'linkname' header is limited to
437
* 100 bytes (-1 byte for terminating zero), if path is greater than that,
438
* store symlink as a plain-text file. (Not sure how TAR handles long links.)
439
*/
440
if( mPerm == PERM_LNK && n <= 100 ){
441
sqlite3_snprintf(100, (char*)&tball.aHdr[157], "%s", blob_str(pContent));
442
cType = '2';
443
n = 0;
444
}
445
446
tar_add_header(zName, nName, ( mPerm==PERM_EXE ) ? 0755 : 0644,
447
mTime, n, cType);
448
if( n ){
449
gzip_step(blob_buffer(pContent), n);
450
lastPage = n % 512;
451
if( lastPage!=0 ){
452
gzip_step(tball.zSpaces, 512 - lastPage);
453
}
454
}
455
}
456
457
/*
458
** Finish constructing the tarball. Put the content of the tarball
459
** in Blob pOut.
460
*/
461
static void tar_finish(Blob *pOut){
462
db_multi_exec("DROP TABLE dir");
463
gzip_step(tball.zSpaces, 512);
464
gzip_step(tball.zSpaces, 512);
465
gzip_finish(pOut);
466
fossil_free(tball.aHdr);
467
tball.aHdr = 0;
468
fossil_free(tball.zPrevDir);
469
tball.zPrevDir = NULL;
470
tball.nPrevDirAlloc = 0;
471
blob_reset(&tball.pax);
472
}
473
474
475
/*
476
** COMMAND: test-tarball
477
**
478
** Generate a GZIP-compressed tarball in the file given by the first argument
479
** that contains files given in the second and subsequent arguments.
480
**
481
** -h|--dereference Follow symlinks and archive the files they point to
482
*/
483
void test_tarball_cmd(void){
484
int i;
485
Blob zip;
486
int eFType = SymFILE;
487
if( g.argc<3 ){
488
usage("ARCHIVE [options] FILE....");
489
}
490
if( find_option("dereference","h",0) ){
491
eFType = ExtFILE;
492
}
493
sqlite3_open(":memory:", &g.db);
494
tar_begin(-1);
495
for(i=3; i<g.argc; i++){
496
Blob file;
497
blob_zero(&file);
498
blob_read_from_file(&file, g.argv[i], eFType);
499
tar_add_file(g.argv[i], &file, file_perm(0,eFType), file_mtime(0,eFType));
500
blob_reset(&file);
501
}
502
tar_finish(&zip);
503
blob_write_to_file(&zip, g.argv[2]);
504
}
505
506
/*
507
** Given the RID for a check-in, construct a tarball containing
508
** all files in that check-in that match pGlob (or all files if
509
** pGlob is NULL).
510
**
511
** If RID is for an object that is not a real manifest, then the
512
** resulting tarball contains a single file which is the RID
513
** object. pInclude and pExclude are ignored in this case.
514
**
515
** If the RID object does not exist in the repository, then
516
** pTar is zeroed.
517
**
518
** zDir is a "synthetic" subdirectory which all files get
519
** added to as part of the tarball. It may be 0 or an empty string, in
520
** which case it is ignored. The intention is to create a tarball which
521
** politely expands into a subdir instead of filling your current dir
522
** with source files. For example, pass an artifact hash or "ProjectName".
523
**
524
*/
525
void tarball_of_checkin(
526
int rid, /* The RID of the check-in from which to form a tarball*/
527
Blob *pTar, /* Write the tarball into this blob */
528
const char *zDir, /* Directory prefix for all file added to tarball */
529
Glob *pInclude, /* Only add files matching this pattern */
530
Glob *pExclude, /* Exclude files matching this pattern */
531
int listFlag /* Show filenames on stdout */
532
){
533
Blob mfile, hash, file;
534
Manifest *pManifest;
535
ManifestFile *pFile;
536
Blob filename;
537
int nPrefix;
538
char *zName = 0;
539
unsigned int mTime;
540
541
content_get(rid, &mfile);
542
if( blob_size(&mfile)==0 ){
543
blob_zero(pTar);
544
return;
545
}
546
blob_set_dynamic(&hash, rid_to_uuid(rid));
547
blob_zero(&filename);
548
549
if( zDir && zDir[0] ){
550
blob_appendf(&filename, "%s/", zDir);
551
}
552
nPrefix = blob_size(&filename);
553
554
pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
555
if( pManifest ){
556
int flg, eflg = 0;
557
mTime = (unsigned)((pManifest->rDate - 2440587.5)*86400.0);
558
if( pTar ) tar_begin(mTime);
559
flg = db_get_manifest_setting(blob_str(&hash));
560
if( flg ){
561
/* eflg is the effective flags, taking include/exclude into account */
562
if( (pInclude==0 || glob_match(pInclude, "manifest"))
563
&& !glob_match(pExclude, "manifest")
564
&& (flg & MFESTFLG_RAW) ){
565
eflg |= MFESTFLG_RAW;
566
}
567
if( (pInclude==0 || glob_match(pInclude, "manifest.uuid"))
568
&& !glob_match(pExclude, "manifest.uuid")
569
&& (flg & MFESTFLG_UUID) ){
570
eflg |= MFESTFLG_UUID;
571
}
572
if( (pInclude==0 || glob_match(pInclude, "manifest.tags"))
573
&& !glob_match(pExclude, "manifest.tags")
574
&& (flg & MFESTFLG_TAGS) ){
575
eflg |= MFESTFLG_TAGS;
576
}
577
578
if( eflg & (MFESTFLG_RAW|MFESTFLG_UUID) ){
579
if( eflg & MFESTFLG_RAW ){
580
blob_append(&filename, "manifest", -1);
581
zName = blob_str(&filename);
582
if( listFlag ) fossil_print("%s\n", zName);
583
if( pTar ){
584
tar_add_file(zName, &mfile, 0, mTime);
585
}
586
}
587
}
588
blob_reset(&mfile);
589
if( eflg & MFESTFLG_UUID ){
590
blob_resize(&filename, nPrefix);
591
blob_append(&filename, "manifest.uuid", -1);
592
zName = blob_str(&filename);
593
if( listFlag ) fossil_print("%s\n", zName);
594
if( pTar ){
595
blob_append(&hash, "\n", 1);
596
tar_add_file(zName, &hash, 0, mTime);
597
}
598
}
599
if( eflg & MFESTFLG_TAGS ){
600
blob_resize(&filename, nPrefix);
601
blob_append(&filename, "manifest.tags", -1);
602
zName = blob_str(&filename);
603
if( listFlag ) fossil_print("%s\n", zName);
604
if( pTar ){
605
Blob tagslist;
606
blob_zero(&tagslist);
607
get_checkin_taglist(rid, &tagslist);
608
tar_add_file(zName, &tagslist, 0, mTime);
609
blob_reset(&tagslist);
610
}
611
}
612
}
613
manifest_file_rewind(pManifest);
614
while( (pFile = manifest_file_next(pManifest,0))!=0 ){
615
int fid;
616
if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue;
617
if( glob_match(pExclude, pFile->zName) ) continue;
618
fid = uuid_to_rid(pFile->zUuid, 0);
619
if( fid ){
620
blob_resize(&filename, nPrefix);
621
blob_append(&filename, pFile->zName, -1);
622
zName = blob_str(&filename);
623
if( listFlag ) fossil_print("%s\n", zName);
624
if( pTar ){
625
content_get(fid, &file);
626
tar_add_file(zName, &file, manifest_file_mperm(pFile), mTime);
627
blob_reset(&file);
628
}
629
}
630
}
631
}else{
632
blob_append(&filename, blob_str(&hash), 16);
633
zName = blob_str(&filename);
634
if( listFlag ) fossil_print("%s\n", zName);
635
if( pTar ){
636
mTime = db_int64(0, "SELECT (julianday('now') - 2440587.5)*86400.0;");
637
tar_begin(mTime);
638
tar_add_file(zName, &mfile, 0, mTime);
639
}
640
}
641
manifest_destroy(pManifest);
642
blob_reset(&mfile);
643
blob_reset(&hash);
644
blob_reset(&filename);
645
if( pTar ) tar_finish(pTar);
646
}
647
648
/*
649
** COMMAND: tarball*
650
**
651
** Usage: %fossil tarball VERSION OUTPUTFILE [OPTIONS]
652
**
653
** Generate a compressed tarball for a specified version. If the --name
654
** option is used, its argument becomes the name of the top-level directory
655
** in the resulting tarball. If --name is omitted, the top-level directory
656
** name is derived from the project name, the check-in date and time, and
657
** the artifact ID of the check-in.
658
**
659
** The GLOBLIST argument to --exclude and --include can be a comma-separated
660
** list of glob patterns, where each glob pattern may optionally be enclosed
661
** in "..." or '...' so that it may contain commas. If a file matches both
662
** --include and --exclude then it is excluded.
663
**
664
** If OUTPUTFILE is an empty string or "/dev/null" then no tarball is
665
** actually generated. This feature can be used in combination with
666
** the --list option to get a list of the filenames that would be in the
667
** tarball had it actually been generated. Note that --list shows only
668
** filenames. "tar tzf" shows both filenames and subdirectory names.
669
**
670
** Options:
671
** -X|--exclude GLOBLIST Comma-separated list of GLOBs of files to exclude
672
** --include GLOBLIST Comma-separated list of GLOBs of files to include
673
** -l|--list Show archive content on stdout
674
** --name DIRECTORYNAME The name of the top-level directory in the archive
675
** -R REPOSITORY Specify a Fossil repository
676
*/
677
void tarball_cmd(void){
678
int rid;
679
Blob tarball;
680
const char *zName;
681
Glob *pInclude = 0;
682
Glob *pExclude = 0;
683
const char *zInclude;
684
const char *zExclude;
685
int listFlag = 0;
686
const char *zOut;
687
zName = find_option("name", 0, 1);
688
zExclude = find_option("exclude", "X", 1);
689
if( zExclude ) pExclude = glob_create(zExclude);
690
zInclude = find_option("include", 0, 1);
691
if( zInclude ) pInclude = glob_create(zInclude);
692
db_find_and_open_repository(0, 0);
693
listFlag = find_option("list","l",0)!=0;
694
695
/* We should be done with options.. */
696
verify_all_options();
697
698
if( g.argc!=4 ){
699
usage("VERSION OUTPUTFILE");
700
}
701
g.zOpenRevision = g.argv[2];
702
rid = name_to_typed_rid(g.argv[2], "ci");
703
if( rid==0 ){
704
fossil_fatal("Check-in not found: %s", g.argv[2]);
705
return;
706
}
707
zOut = g.argv[3];
708
if( fossil_strcmp("/dev/null",zOut)==0 || fossil_strcmp("",zOut)==0 ){
709
zOut = 0;
710
}
711
712
if( zName==0 ){
713
zName = archive_base_name(rid);
714
}
715
tarball_of_checkin(rid, zOut ? &tarball : 0,
716
zName, pInclude, pExclude, listFlag);
717
glob_free(pInclude);
718
glob_free(pExclude);
719
if( listFlag ) fflush(stdout);
720
if( zOut ){
721
blob_write_to_file(&tarball, zOut);
722
blob_reset(&tarball);
723
}
724
}
725
726
/*
727
** This is a helper routine for tar_uuid_from_name(). It handles
728
** the case where *pzName contains no "/" character. Check for
729
** format (3). Return the hash if the name matches format (3),
730
** or return NULL if it does not.
731
*/
732
static char *format_three_parser(const char *zName){
733
int iDot = 0; /* Index in zName[] of the first '.' */
734
int iDash1 = 0; /* Index in zName[] of the '-' before the timestamp */
735
int iDash2 = 0; /* Index in zName[] of the '-' between timestamp and hash */
736
int nHash; /* Size of the hash */
737
char *zHash; /* A copy of the hash value */
738
char *zDate; /* Copy of the timestamp */
739
char *zUuid; /* Final result */
740
int i; /* Loop query */
741
Stmt q; /* Query to verify that hash and timestamp agree */
742
743
for(i=0; zName[i]; i++){
744
char c = zName[i];
745
if( c=='.' ){ iDot = i; break; }
746
if( c=='-' ){ iDash1 = iDash2; iDash2 = i; }
747
if( !fossil_isalnum(c) && c!='_' && c!='-' ){ break; }
748
}
749
if( iDot==0 ) return 0;
750
if( iDash1==0 ) return 0;
751
nHash = iDot - iDash2 - 1;
752
if( nHash<8 ) return 0; /* HASH value too short */
753
if( (iDash2 - iDash1)!=15 ) return 0; /* Wrong timestamp size */
754
zHash = fossil_strndup(&zName[iDash2+1], nHash);
755
zDate = fossil_strndup(&zName[iDash1+1], 14);
756
db_prepare(&q,
757
"SELECT blob.uuid"
758
" FROM blob JOIN event ON event.objid=blob.rid"
759
" WHERE blob.uuid GLOB '%q*'"
760
" AND strftime('%%Y%%m%%d%%H%%M%%S',event.mtime)='%q'",
761
zHash, zDate
762
);
763
fossil_free(zHash);
764
fossil_free(zDate);
765
if( db_step(&q)==SQLITE_ROW ){
766
zUuid = fossil_strdup(db_column_text(&q,0));
767
}else{
768
zUuid = 0;
769
}
770
db_finalize(&q);
771
return zUuid;
772
}
773
774
/*
775
** Check to see if the input string is of one of the following
776
** two the forms:
777
**
778
** check-in-name/filename.ext (1)
779
** tag-name/check-in-name/filename.ext (2)
780
** project-datetime-hash.ext (3)
781
**
782
** In other words, check to see if the input string contains either
783
** a check-in name or a tag-name and a check-in name separated by
784
** a slash. There must be between 0 or 2 "/" characters. In the
785
** second form, tag-name must be an individual tag (not a branch-tag)
786
** that is found on the check-in identified by the check-in-name.
787
**
788
** If the condition is true, then:
789
**
790
** * Make *pzName point to the filename suffix only
791
** * return a copy of the check-in name in memory from mprintf().
792
**
793
** If the condition is false, leave *pzName unchanged and return either
794
** NULL or an empty string. Normally NULL is returned, however an
795
** empty string is returned for format (2) if check-in-name does not
796
** match tag-name.
797
**
798
** Format (2) is specifically designed to allow URLs like this:
799
**
800
** /tarball/release/UUID/PROJECT.tar.gz
801
**
802
** Such URLs will pass through most anti-robot filters because of the
803
** "/tarball/release" prefix will match the suggested "robot-exception"
804
** pattern and can still refer to an historic release rather than just
805
** the most recent release.
806
**
807
** Format (3) is designed to allow URLs like this:
808
**
809
** /tarball/fossil-20251018193920-d6c9aee97df.tar.gz
810
**
811
** In other words, filename itself contains sufficient information to
812
** uniquely identify the check-in, including a timestamp of the form
813
** YYYYMMDDHHMMSS and a prefix of the check-in hash. The timestamp
814
** and hash must immediately precede the first "." in the name.
815
*/
816
char *tar_uuid_from_name(char **pzName){
817
char *zName = *pzName; /* Original input */
818
int n1 = 0; /* Bytes in first prefix (tag-name) */
819
int n2 = 0; /* Bytes in second prefix (check-in-name) */
820
int n = 0; /* max(n1,n2) */
821
int i; /* Loop counter */
822
for(i=n1=n2=0; zName[i]; i++){
823
if( zName[i]=='/' ){
824
if( n1==0 ){
825
n = n1 = i;
826
}else if( n2==0 ){
827
n = n2 = i;
828
}else{
829
return 0; /* More than two "/" characters seen */
830
}
831
}
832
}
833
if( n1==0 ){
834
/* Check for format (3) */
835
return format_three_parser(*pzName);
836
}
837
if( zName[n+1]==0 ){
838
return 0; /* No filename suffix */
839
}
840
if( n2==0 ){
841
/* Format (1): check-in name only. The check-in-name is not verified */
842
zName[n1] = 0;
843
*pzName = fossil_strdup(&zName[n1+1]);
844
return zName;
845
}else if( n2>n1+1 ){
846
/* Format (2): tag-name/check-in-name. Verify that check-in-name is real
847
** and that the check-in has the tag named by tag-name.
848
*/
849
char *zCkin = mprintf("%.*s", n2-n1-1, &zName[n1+1]);
850
char *zTag;
851
int rid = symbolic_name_to_rid(zCkin,"ci");
852
int hasTag;
853
if( rid<=0 ){
854
fossil_free(zCkin);
855
return fossil_strdup("");
856
}
857
zTag = mprintf("%.*s", n1, zName);
858
hasTag = db_exists(
859
"SELECT 1 FROM tagxref, tag"
860
" WHERE tagxref.rid=%d"
861
" AND tag.tagid=tagxref.tagid"
862
" AND tagxref.tagtype=1"
863
" AND tag.tagname='sym-%q'",
864
rid, zTag
865
);
866
fossil_free(zTag);
867
if( !hasTag ){
868
fossil_free(zCkin);
869
return fossil_strdup("");
870
}
871
*pzName = fossil_strdup(&zName[n2+1]);
872
return zCkin;
873
}else{
874
return 0;
875
}
876
}
877
878
/*
879
** WEBPAGE: tarball
880
** URL: /tarball/NAME.tar.gz
881
** or: /tarball/VERSION/NAME.tar.gz
882
** or: /tarball/TAG/VERSION/NAME.tar.gz
883
**
884
** Generate a compressed tarball for the check-in specified by VERSION.
885
** The tarball is called NAME.tar.gz and has a top-level directory called
886
** NAME. If TAG is provided, then VERSION must hold TAG or else an error
887
** is returned.
888
**
889
** The optional VERSION element defaults to the name of the main branch
890
** (usually "trunk") per the r= rules below.
891
** All of the following URLs are equivalent:
892
**
893
** /tarball/release/xyz.tar.gz
894
** /tarball?r=release&name=xyz.tar.gz
895
** /tarball/xyz.tar.gz?r=release
896
** /tarball?name=release/xyz.tar.gz
897
**
898
** Query parameters:
899
**
900
** name=[CKIN/]NAME The optional CKIN component of the name= parameter
901
** identifies the check-in from which the tarball is
902
** constructed. If CKIN is omitted and there is no
903
** r= query parameter, then use the name of the main
904
** branch (usually "trunk"). NAME is the
905
** name of the download file. The top-level directory
906
** in the generated tarball is called by NAME with the
907
** file extension removed.
908
**
909
** r=TAG TAG identifies the check-in that is turned into a
910
** compressed tarball. The default value is the name of
911
** the main branch (usually "trunk").
912
** If r= is omitted and if the name= query parameter
913
** contains one "/" character then the of part the
914
** name= value before the / becomes the TAG and the
915
** part of the name= value after the / is the download
916
** filename. If no check-in is specified by either
917
** name= or r=, then the name of the main branch
918
** (usually "trunk") is used.
919
**
920
** in=PATTERN Only include files that match the comma-separated
921
** list of GLOB patterns in PATTERN, as with ex=
922
**
923
** ex=PATTERN Omit any file that match PATTERN. PATTERN is a
924
** comma-separated list of GLOB patterns, where each
925
** pattern can optionally be quoted using ".." or '..'.
926
** Any file matching both ex= and in= is excluded.
927
**
928
** Robot Defenses:
929
**
930
** * If "zip" appears in the robot-restrict setting, then robots are
931
** not allowed to access this page. Suspected robots will be
932
** presented with a captcha.
933
**
934
** * If "zipX" appears in the robot-restrict setting, then robots are
935
** restricted in the same way as with "zip", but with exceptions.
936
** If the check-in for which an archive is requested is a leaf check-in
937
** and if the robot-zip-leaf setting is true, then the request is
938
** allowed. Or if the check-in has a tag that matches any of the
939
** GLOB patterns on the list in the robot-zip-tag setting, then the
940
** request is allowed. Otherwise, the usual robot defenses are
941
** activated.
942
*/
943
void tarball_page(void){
944
int rid;
945
char *zName, *zRid, *zKey;
946
int nName, nRid;
947
const char *zInclude; /* The in= query parameter */
948
const char *zExclude; /* The ex= query parameter */
949
Blob cacheKey; /* The key to cache */
950
Glob *pInclude = 0; /* The compiled in= glob pattern */
951
Glob *pExclude = 0; /* The compiled ex= glob pattern */
952
Blob tarball; /* Tarball accumulated here */
953
const char *z;
954
955
login_check_credentials();
956
if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
957
if( robot_restrict("zip") ) return;
958
fossil_nice_default();
959
zName = fossil_strdup(PD("name",""));
960
z = P("r");
961
if( z==0 ) z = P("uuid");
962
if( z==0 ) z = tar_uuid_from_name(&zName);
963
if( z==0 ) z = fossil_strdup(db_main_branch());
964
g.zOpenRevision = zRid = fossil_strdup(z);
965
nRid = strlen(zRid);
966
zInclude = P("in");
967
if( zInclude ) pInclude = glob_create(zInclude);
968
zExclude = P("ex");
969
if( zExclude ) pExclude = glob_create(zExclude);
970
if( zInclude==0 && zExclude==0 ){
971
etag_check_for_invariant_name(z);
972
}
973
nName = strlen(zName);
974
if( nName>7 && fossil_strcmp(&zName[nName-7], ".tar.gz")==0 ){
975
/* Special case: Remove the ".tar.gz" suffix. */
976
nName -= 7;
977
zName[nName] = 0;
978
}else{
979
/* If the file suffix is not ".tar.gz" then just remove the
980
** suffix up to and including the last "." */
981
for(nName=strlen(zName)-1; nName>5; nName--){
982
if( zName[nName]=='.' ){
983
zName[nName] = 0;
984
break;
985
}
986
}
987
}
988
rid = symbolic_name_to_rid(nRid?zRid:zName, "ci");
989
if( rid==0 ){
990
cgi_set_status(404, "Not Found");
991
@ Not found
992
return;
993
}
994
if( robot_restrict_zip(rid) ) return;
995
if( nRid==0 && nName>10 ) zName[10] = 0;
996
997
/* Compute a unique key for the cache entry based on query parameters */
998
blob_init(&cacheKey, 0, 0);
999
blob_appendf(&cacheKey, "/tarball/%z", rid_to_uuid(rid));
1000
blob_appendf(&cacheKey, "/%q", zName);
1001
if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude);
1002
if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude);
1003
zKey = blob_str(&cacheKey);
1004
etag_check(ETAG_HASH, zKey);
1005
1006
if( P("debug")!=0 ){
1007
style_header("Tarball Generator Debug Screen");
1008
@ zName = "%h(zName)"<br>
1009
@ rid = %d(rid)<br>
1010
if( zInclude ){
1011
@ zInclude = "%h(zInclude)"<br>
1012
}
1013
if( zExclude ){
1014
@ zExclude = "%h(zExclude)"<br>
1015
}
1016
@ zKey = "%h(zKey)"
1017
style_finish_page();
1018
return;
1019
}
1020
if( referred_from_login() ){
1021
style_header("Tarball Download");
1022
@ <form action='%R/tarball/%h(zName).tar.gz'>
1023
cgi_query_parameters_to_hidden();
1024
@ <p>Tarball named <b>%h(zName).tar.gz</b> holding the content
1025
@ of check-in <b>%h(zRid)</b>:
1026
@ <input type="submit" value="Download">
1027
@ </form>
1028
style_finish_page();
1029
return;
1030
}
1031
cgi_check_for_malice();
1032
blob_zero(&tarball);
1033
if( cache_read(&tarball, zKey)==0 ){
1034
tarball_of_checkin(rid, &tarball, zName, pInclude, pExclude, 0);
1035
cache_write(&tarball, zKey);
1036
}
1037
glob_free(pInclude);
1038
glob_free(pExclude);
1039
fossil_free(zName);
1040
fossil_free(zRid);
1041
g.zOpenRevision = 0;
1042
blob_reset(&cacheKey);
1043
cgi_set_content(&tarball);
1044
cgi_set_content_type("application/x-compressed");
1045
}
1046
1047
/*
1048
** This routine is called for each check-in on the /download page to
1049
** construct the "extra" information after the description.
1050
*/
1051
void download_extra(
1052
Stmt *pQuery, /* Current row of the timeline query */
1053
int tmFlags, /* Flags to www_print_timeline() */
1054
const char *zThisUser, /* Suppress links to this user */
1055
const char *zThisTag /* Suppress links to this tag */
1056
){
1057
const char *zType = db_column_text(pQuery, 7);
1058
assert( zType!=0 );
1059
if( zType[0]!='c' ){
1060
timeline_extra(pQuery, tmFlags, zThisUser, zThisTag);
1061
}else{
1062
int rid = db_column_int(pQuery, 0);
1063
const char *zUuid = db_column_text(pQuery, 1);
1064
char *zBrName = branch_of_rid(rid);
1065
char *zNm;
1066
1067
if( tmFlags & TIMELINE_COLUMNAR ){
1068
@ <nobr>check-in:&nbsp;\
1069
@ %z(href("%R/info/%!S",zUuid))<span class='timelineHash'>\
1070
@ %S(zUuid)</span></a></nobr><br>
1071
if( fossil_strcmp(zBrName,"trunk")!=0 ){
1072
@ <nobr>branch:&nbsp;\
1073
@ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a></nobr><br>\
1074
}
1075
}else{
1076
if( (tmFlags & TIMELINE_CLASSIC)==0 ){
1077
@ check-in:&nbsp;%z(href("%R/info/%!S",zUuid))\
1078
@ <span class='timelineHash'>%S(zUuid)</span></a>
1079
}
1080
if( (tmFlags & TIMELINE_GRAPH)==0 && fossil_strcmp(zBrName,"trunk")!=0 ){
1081
@ branch:&nbsp;\
1082
@ %z(href("%R/timeline?r=%t",zBrName))%h(zBrName)</a>
1083
}
1084
}
1085
zNm = archive_base_name(rid);
1086
@ %z(href("%R/tarball/%s.tar.gz",zNm))\
1087
@ <button>Tarball</button></a>
1088
@ %z(href("%R/zip/%s.zip",zNm))\
1089
@ <button>ZIP&nbsp;Archive</button></a>
1090
fossil_free(zBrName);
1091
fossil_free(zNm);
1092
}
1093
}
1094
1095
/*
1096
** SETTING: suggested-downloads width=70 block-text
1097
**
1098
** This setting controls the suggested tarball/ZIP downloads on the
1099
** [[/download]] page. The value is a TCL list. Each set of four items
1100
** defines a set of check-ins to be added to the suggestion list.
1101
** The items in each group are:
1102
**
1103
** | COUNT TAG MAX_AGE COMMENT
1104
**
1105
** COUNT is the number of check-ins to match, starting with the most
1106
** recent and working bacwards in time. Check-ins match if they contain
1107
** the tag TAG. If MAX_AGE is not an empty string, then it specifies
1108
** the maximum age of any matching check-in. COMMENT is an optional
1109
** comment for each match.
1110
**
1111
** The special value of "OPEN-LEAF" for TAG matches any check-in that
1112
** is an open leaf.
1113
**
1114
** MAX_AGE is of the form "{AMT UNITS}" where AMT is a floating point
1115
** value and UNITS is one of "seconds", "hours", "days", "weeks", "months",
1116
** or "years". If MAX_AGE is an empty string then there is no age limit.
1117
**
1118
** If COMMENT is not an empty string, then it is an additional comment
1119
** added to the output description of the suggested download. The idea of
1120
** COMMENT is to explain to the reader why a check-in is a suggested
1121
** download.
1122
**
1123
** Example:
1124
**
1125
** | 1 trunk {} {Latest Trunk Check-in}
1126
** | 5 OPEN-LEAF {1 month} {Active Branch}
1127
** | 999 release {1 year} {Official Release}
1128
**
1129
** The value causes the /download page to show the union of the most
1130
** recent trunk check-in of any age, the five most recent
1131
** open leaves within the past month, and essentially
1132
** all releases within the past year. If the same check-in matches more
1133
** than one rule, the COMMENT of the first match is used.
1134
*/
1135
1136
/*
1137
** WEBPAGE: /download
1138
**
1139
** Show a special no-graph timeline of recent important check-ins with
1140
** an opportunity to pull tarballs and ZIPs.
1141
*/
1142
void download_page(void){
1143
Stmt q; /* The actual timeline query */
1144
const char *zTarlistCfg; /* Configurag; /* Configuration string */
1145
char **azItem; /* Decomposed elements of zTarlistCfg */
1146
int *anItem; /* Bytes in each term of azItem[] */
1147
int nItem; /* Number of terms in azItem[] */
1148
int i; /* Loop counter */
1149
int tmFlags; /* Timeline display flags */
1150
int n; /* Number of suggested downloads */
1151
double rNow; /* Current time. Julian day number */
1152
int bPlainTextCom; /* Use plain-text comments */
1153
1154
login_check_credentials();
1155
if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
1156
1157
style_set_current_feature("timeline");
1158
style_header("Suggested Downloads");
1159
1160
zTarlistCfg = db_get("suggested-downloads","off");
1161
db_multi_exec(
1162
"CREATE TEMP TABLE tarlist(rid INTEGER PRIMARY KEY, com TEXT);"
1163
);
1164
rNow = db_double(0.0,"SELECT julianday()");
1165
if( !g.interp ) Th_FossilInit(0);
1166
Th_SplitList(g.interp, zTarlistCfg, (int)strlen(zTarlistCfg),
1167
&azItem, &anItem, &nItem);
1168
bPlainTextCom = db_get_boolean("timeline-plaintext",0);
1169
for(i=0; i<nItem-3; i+=4){
1170
int cnt; /* The number of instances of zLabel to use */
1171
char *zLabel; /* The label to match */
1172
double rStart; /* Starting time, Julian day number */
1173
char *zComment = 0; /* Comment to apply */
1174
if( anItem[i]==1 && azItem[i][0]=='*' ){
1175
cnt = -1;
1176
}else if( anItem[i]<1 ){
1177
cnt = 0;
1178
}else{
1179
cnt = atoi(azItem[i]);
1180
}
1181
if( cnt==0 ) continue;
1182
zLabel = fossil_strndup(azItem[i+1],anItem[i+1]);
1183
if( anItem[i+2]==0 ){
1184
rStart = 0.0;
1185
}else{
1186
char *zMax = fossil_strndup(azItem[i+2], anItem[i+2]);
1187
double r = fossil_atof(zMax);
1188
if( strstr(zMax,"sec") ){
1189
rStart = rNow - r/86400.0;
1190
}else
1191
if( strstr(zMax,"hou") ){
1192
rStart = rNow - r/24.0;
1193
}else
1194
if( strstr(zMax,"da") ){
1195
rStart = rNow - r;
1196
}else
1197
if( strstr(zMax,"wee") ){
1198
rStart = rNow - r*7.0;
1199
}else
1200
if( strstr(zMax,"mon") ){
1201
rStart = rNow - r*30.44;
1202
}else
1203
if( strstr(zMax,"yea") ){
1204
rStart = rNow - r*365.24;
1205
}else
1206
{ /* Default to seconds */
1207
rStart = rNow - r/86400.0;
1208
}
1209
}
1210
if( anItem[i+3]==0 ){
1211
zComment = fossil_strdup("");
1212
}else if( bPlainTextCom ){
1213
zComment = mprintf("** %.*s ** ", anItem[i+3], azItem[i+3]);
1214
}else{
1215
zComment = mprintf("<b>%.*s</b>\n<p>", anItem[i+3], azItem[i+3]);
1216
}
1217
if( fossil_strcmp("OPEN-LEAF",zLabel)==0 ){
1218
db_multi_exec(
1219
"INSERT OR IGNORE INTO tarlist(rid,com)"
1220
" SELECT leaf.rid, %Q FROM leaf, event"
1221
" WHERE event.objid=leaf.rid"
1222
" AND event.mtime>=%.6f"
1223
" AND NOT EXISTS(SELECT 1 FROM tagxref"
1224
" WHERE tagxref.rid=leaf.rid"
1225
" AND tagid=%d AND tagtype>0)"
1226
" ORDER BY event.mtime DESC LIMIT %d",
1227
zComment, rStart, TAG_CLOSED, cnt
1228
);
1229
}else{
1230
db_multi_exec(
1231
"WITH taglist(tid) AS"
1232
" (SELECT tagid FROM tag WHERE tagname GLOB 'sym-%q')"
1233
"INSERT OR IGNORE INTO tarlist(rid,com)"
1234
" SELECT event.objid, %Q FROM event CROSS JOIN tagxref"
1235
" WHERE event.type='ci'"
1236
" AND event.mtime>=%.6f"
1237
" AND tagxref.tagid IN taglist"
1238
" AND tagtype>0"
1239
" AND tagxref.rid=event.objid"
1240
" ORDER BY event.mtime DESC LIMIT %d",
1241
zLabel, zComment, rStart, cnt
1242
);
1243
}
1244
fossil_free(zLabel);
1245
fossil_free(zComment);
1246
}
1247
Th_Free(g.interp, azItem);
1248
1249
n = db_int(0, "SELECT count(*) FROM tarlist");
1250
if( n==0 ){
1251
@ <h2>No tarball/ZIP suggestions are available at this time</h2>
1252
}else{
1253
@ <h2>%d(n) Tarball/ZIP Download Suggestion%s(n>1?"s":""):</h2>
1254
db_prepare(&q,
1255
"WITH matches AS (%s AND blob.rid IN (SELECT rid FROM tarlist))\n"
1256
"SELECT blobRid, uuid, timestamp,"
1257
" com||comment,"
1258
" user, leaf, bgColor, eventType, tags, tagid, brief, mtime"
1259
" FROM matches JOIN tarlist ON tarlist.rid=blobRid"
1260
" ORDER BY matches.mtime DESC",
1261
timeline_query_for_www()
1262
);
1263
1264
tmFlags = TIMELINE_DISJOINT | TIMELINE_NOSCROLL | TIMELINE_COLUMNAR
1265
| TIMELINE_BRCOLOR;
1266
www_print_timeline(&q, tmFlags, 0, 0, 0, 0, 0, download_extra);
1267
db_finalize(&q);
1268
}
1269
if( g.perm.Clone ){
1270
char *zNm = fossil_strdup(db_get("project-name","clone"));
1271
sanitize_name(zNm);
1272
@ <hr>
1273
@ <h2>You Can Clone This Repository</h2>
1274
@
1275
@ <p>Clone this repository by running a command similar to the following:
1276
@ <blockquote><pre>
1277
@ fossil clone %s(g.zBaseURL) %h(zNm).fossil
1278
@ </pre></blockquote>
1279
@ <p>A clone gives you local access to all historical content.
1280
@ Cloning is a bandwidth- and CPU-efficient alternative to extracting
1281
@ multiple tarballs and ZIPs.
1282
@ Do a web search for "fossil clone" or similar to find additional
1283
@ information about using a cloned Fossil repository. Or ask your
1284
@ favorite AI how to extract content from a Fossil clone.
1285
fossil_free(zNm);
1286
}
1287
1288
style_finish_page();
1289
}
1290
1291
/*
1292
** WEBPAGE: rchvdwnld
1293
**
1294
** Short for "archive download". This page should have a single name=
1295
** query parameter that is a check-in hash or symbolic name. The resulting
1296
** page offers a menu of possible download options for that check-in,
1297
** including tarball, ZIP, or SQLAR.
1298
**
1299
** This is a utility page. The /dir and /tree pages sometimes have a
1300
** "Download" option in their submenu which redirects here. Those pages
1301
** used to have separate "Tarball" and "ZIP" submenu entries, but as
1302
** submenu entries appear in alphabetical order, that caused the two
1303
** submenu entries to be separated from one another, which is distracting.
1304
**
1305
** If the name= does not have a unique resolution, no error is generated.
1306
** Instead, a redirect to the home page for the repository is made.
1307
**
1308
** Robots are excluded from this page if either of the keywords
1309
** "zip" or "download" appear in the [[robot-restrict]] setting.
1310
*/
1311
void rchvdwnld_page(void){
1312
const char *zUuid;
1313
char *zBase;
1314
int nUuid;
1315
int rid;
1316
char *zTags;
1317
login_check_c

Keyboard Shortcuts

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