|
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: \ |
|
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: \ |
|
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: %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: \ |
|
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 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 |