/* ** Copyright (c) 2016 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code used to implement unversioned file interfaces. */ #include "config.h" #include #if defined(FOSSIL_ENABLE_MINIZ) # define MINIZ_HEADER_FILE_ONLY # include "miniz.c" #else # include #endif #include "unversioned.h" #include /* ** SQL code to implement the tables needed by the unversioned. */ static const char zUnversionedInit[] = @ CREATE TABLE IF NOT EXISTS repository.unversioned( @ uvid INTEGER PRIMARY KEY AUTOINCREMENT, -- unique ID for this file @ name TEXT UNIQUE, -- Name of the uv file @ rcvid INTEGER, -- Where received from @ mtime DATETIME, -- timestamp. Seconds since 1970. @ hash TEXT, -- Content hash. NULL if a delete marker @ sz INTEGER, -- size of content after decompression @ encoding INT, -- 0: plaintext. 1: zlib compressed @ content BLOB -- content of the file. NULL if oversized @ ); ; /* ** Make sure the unversioned table exists in the repository. */ void unversioned_schema(void){ if( !db_table_exists("repository", "unversioned") ){ db_multi_exec(zUnversionedInit/*works-like:""*/); } } /* ** Return a string which is the hash of the unversioned content. ** This is the hash used by repositories to compare content before ** exchanging a catalog. So all repositories must compute this hash ** in exactly the same way. ** ** If debugFlag is set, force the value to be recomputed and write ** the text of the hashed string to stdout. */ const char *unversioned_content_hash(int debugFlag){ const char *zHash = debugFlag ? 0 : db_get("uv-hash", 0); if( zHash==0 ){ Stmt q; db_prepare(&q, "SELECT printf('%%s %%s %%s\n',name,datetime(mtime,'unixepoch'),hash)" " FROM unversioned" " WHERE hash IS NOT NULL" " ORDER BY name" ); while( db_step(&q)==SQLITE_ROW ){ const char *z = db_column_text(&q, 0); if( debugFlag ) fossil_print("%s", z); sha1sum_step_text(z,-1); } db_finalize(&q); db_set("uv-hash", sha1sum_finish(0), 0); zHash = db_get("uv-hash",0); } return zHash; } /* ** Initialize pContent to be the content of an unversioned file zName. ** ** Return 0 on success. Return 1 if zName is not found. */ int unversioned_content(const char *zName, Blob *pContent){ Stmt q; int rc = 1; blob_init(pContent, 0, 0); db_prepare(&q, "SELECT encoding, content FROM unversioned WHERE name=%Q", zName); if( db_step(&q)==SQLITE_ROW ){ db_column_blob(&q, 1, pContent); if( db_column_int(&q, 0)==1 ){ blob_uncompress(pContent, pContent); } rc = 0; } db_finalize(&q); return rc; } /* ** Write unversioned content into the database. */ static void unversioned_write( const char *zUVFile, /* Name of the unversioned file */ Blob *pContent, /* File content */ sqlite3_int64 mtime /* Modification time */ ){ Stmt ins; Blob compressed; Blob hash; db_prepare(&ins, "REPLACE INTO unversioned(name,rcvid,mtime,hash,sz,encoding,content)" " VALUES(:name,:rcvid,:mtime,:hash,:sz,:encoding,:content)" ); sha1sum_blob(pContent, &hash); blob_compress(pContent, &compressed); db_bind_text(&ins, ":name", zUVFile); db_bind_int(&ins, ":rcvid", g.rcvid); db_bind_int64(&ins, ":mtime", mtime); db_bind_text(&ins, ":hash", blob_str(&hash)); db_bind_int(&ins, ":sz", blob_size(pContent)); if( blob_size(&compressed) <= 0.8*blob_size(pContent) ){ db_bind_int(&ins, ":encoding", 1); db_bind_blob(&ins, ":content", &compressed); }else{ db_bind_int(&ins, ":encoding", 0); db_bind_blob(&ins, ":content", pContent); } db_step(&ins); blob_reset(&compressed); blob_reset(&hash); db_finalize(&ins); db_unset("uv-hash", 0); } /* ** Check the status of unversioned file zName. "mtime" and "zHash" are the ** time of last change and SHA1 hash of a copy of this file on a remote ** server. Return an integer status code as follows: ** ** 0: zName does not exist in the unversioned table. ** 1: zName exists and should be replaced by the mtime/zHash remote. ** 2: zName exists and is the same as zHash but has a older mtime ** 3: zName exists and is identical to mtime/zHash in all respects. ** 4: zName exists and is the same as zHash but has a newer mtime. ** 5: zName exists and should override the mtime/zHash remote. */ int unversioned_status( const char *zName, sqlite3_int64 mtime, const char *zHash ){ int iStatus = 0; Stmt q; db_prepare(&q, "SELECT mtime, hash FROM unversioned WHERE name=%Q", zName); if( db_step(&q)==SQLITE_ROW ){ const char *zLocalHash = db_column_text(&q, 1); int hashCmp; sqlite3_int64 iLocalMtime = db_column_int64(&q, 0); int mtimeCmp = iLocalMtime=3 ? g.argv[2] : "x"; nCmd = (int)strlen(zCmd); if( zMtime==0 ){ mtime = time(0); }else{ mtime = db_int(0, "SELECT strftime('%%s',%Q)", zMtime); if( mtime<=0 ) fossil_fatal("bad timestamp: %Q", zMtime); } if( memcmp(zCmd, "add", nCmd)==0 ){ const char *zError = 0; const char *zIn; const char *zAs; Blob file; int i; zAs = find_option("as",0,1); if( zAs && g.argc!=4 ) usage("add DISKFILE --as UVFILE"); verify_all_options(); db_begin_transaction(); content_rcvid_init("#!fossil unversioned add"); for(i=3; i1 && zCmd[1]=='i'); verify_all_options(); if( !longFlag ){ if( allFlag ){ db_prepare(&q, "SELECT name FROM unversioned ORDER BY name"); }else{ db_prepare(&q, "SELECT name FROM unversioned WHERE hash IS NOT NULL" " ORDER BY name"); } while( db_step(&q)==SQLITE_ROW ){ fossil_print("%s\n", db_column_text(&q,0)); } }else{ db_prepare(&q, "SELECT hash, datetime(mtime,'unixepoch'), sz, length(content), name" " FROM unversioned" " ORDER BY name;" ); while( db_step(&q)==SQLITE_ROW ){ const char *zHash = db_column_text(&q, 0); const char *zNoContent = ""; if( zHash==0 ){ if( !allFlag ) continue; zHash = "(deleted)"; }else if( db_column_type(&q,3)==SQLITE_NULL ){ zNoContent = " (no content)"; } fossil_print("%12.12s %s %8d %8d %s%s\n", zHash, db_column_text(&q,1), db_column_int(&q,2), db_column_int(&q,3), db_column_text(&q,4), zNoContent ); } } db_finalize(&q); }else if( memcmp(zCmd, "revert", nCmd)==0 ){ unsigned syncFlags = unversioned_sync_flags(SYNC_UNVERSIONED|SYNC_UV_REVERT); g.argv[1] = "sync"; g.argv[2] = "--uv-noop"; sync_unversioned(syncFlags); }else if( memcmp(zCmd, "remove", nCmd)==0 || memcmp(zCmd, "rm", nCmd)==0 || memcmp(zCmd, "delete", nCmd)==0 ){ int i; verify_all_options(); db_begin_transaction(); for(i=3; i @ @ @ @ } @ if( isDeleted ){ sqlite3_snprintf(sizeof(zSzName), zSzName, "Deleted"); zHash = ""; fullSize = 0; @ }else{ approxSizeName(sizeof(zSzName), zSzName, fullSize); iTotalSz += fullSize; cnt++; @ } @ @ @ @ if( g.perm.Admin ){ if( rcvid ){ @ fossil_free(zAge); } db_finalize(&q); if( n ){ approxSizeName(sizeof(zSzName), zSzName, iTotalSz); @ @ @
Name @ Age @ Size @ User @ SHA1 if( g.perm.Admin ){ @ rcvid } @
%h(zName) %h(zName) %s(zAge) %s(zSzName) %h(zLogin) %h(zHash) %d(rcvid) }else{ @ } } @
Total over %d(cnt) files%s(zSzName) @ if( g.perm.Admin ){ @ } @
}else{ @ No unversioned files on this server. } style_footer(); } /* ** WEBPAGE: juvlist ** ** Return a complete list of unversioned files as JSON. The JSON ** looks like this: ** ** [{"name":NAME, ** "mtime":MTIME, ** "hash":HASH, ** "size":SIZE, ** "user":USER}] */ void uvlist_json_page(void){ Stmt q; char *zSep = "["; Blob json; login_check_credentials(); if( !g.perm.Read ){ login_needed(g.anon.Read); return; } cgi_set_content_type("text/json"); if( !db_table_exists("repository","unversioned") ){ blob_init(&json, "[]", -1); cgi_set_content(&json); return; } blob_init(&json, 0, 0); db_prepare(&q, "SELECT" " name," " mtime," " hash," " sz," " (SELECT login FROM rcvfrom, user" " WHERE user.uid=rcvfrom.uid AND rcvfrom.rcvid=unversioned.rcvid)" " FROM unversioned WHERE hash IS NOT NULL" ); while( db_step(&q)==SQLITE_ROW ){ const char *zName = db_column_text(&q, 0); sqlite3_int64 mtime = db_column_int(&q, 1); const char *zHash = db_column_text(&q, 2); int fullSize = db_column_int(&q, 3); const char *zLogin = db_column_text(&q, 4); if( zLogin==0 ) zLogin = ""; blob_appendf(&json, "%s{\"name\":\"", zSep); zSep = ",\n "; blob_append_json_string(&json, zName); blob_appendf(&json, "\",\n \"mtime\":%lld,\n \"hash\":\"", mtime); blob_append_json_string(&json, zHash); blob_appendf(&json, "\",\n \"size\":%d,\n \"user\":\"", fullSize); blob_append_json_string(&json, zLogin); blob_appendf(&json, "\"}"); } db_finalize(&q); blob_appendf(&json,"]\n"); cgi_set_content(&json); }