Files
hubicfuse/commonfs.c
T
2016-01-14 23:57:42 +02:00

680 lines
19 KiB
C

#define _GNU_SOURCE
#include <stdio.h>
#include <magic.h>
#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef __linux__
#include <alloca.h>
#endif
#include <pthread.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/syscall.h>
#include <openssl/md5.h>
#include <pwd.h>
#include <fuse.h>
#include <limits.h>
#include <curl/curl.h>
#include <curl/easy.h>
#include "commonfs.h"
#include "config.h"
pthread_mutex_t dcachemut;
pthread_mutexattr_t mutex_attr;
dir_cache* dcache;
char* temp_dir;
int cache_timeout;
int debug = 0;
int verify_ssl = 2;
bool option_get_extended_metadata = false;
bool option_curl_verbose = false;
int option_cache_statfs_timeout = 0;
int option_debug_level = 0;
int option_curl_progress_state = 1;//1 to disable curl progress
bool option_enable_chown = false;
bool option_enable_chmod = false;
bool option_enable_progressive_upload = false;
bool option_enable_progressive_download = false;
size_t file_buffer_size = 0;
// needed to get correct GMT / local time
// hubic stores time as GMT so we have to do conversions
// http://zhu-qy.blogspot.ro/2012/11/ref-how-to-convert-from-utc-to-local.html
time_t my_timegm(struct tm* tm)
{
time_t epoch = 0;
time_t offset = mktime(gmtime(&epoch));
time_t utc = mktime(tm);
return difftime(utc, offset);
}
//expect time_str as a friendly string format
time_t get_time_from_str_as_gmt(char* time_str)
{
struct tm val_time_tm;
time_t val_time_t;
strptime(time_str, "%FT%T", &val_time_tm);
val_time_tm.tm_isdst = -1;
val_time_t = my_timegm(&val_time_tm);
return val_time_t;
}
time_t get_time_as_local(time_t time_t_val, char time_str[], int char_buf_size)
{
struct tm loc_time_tm;
loc_time_tm = *localtime(&time_t_val);
if (time_str != NULL)
{
//debugf(DBG_LEVEL_NORM, 0,"Local len=%d size=%d pass=%d", strlen(time_str), sizeof(time_str), char_buf_size);
strftime(time_str, char_buf_size, "%c", &loc_time_tm);
//debugf(DBG_LEVEL_NORM, 0,"Local timestr=[%s] size=%d", time_str, strlen(time_str));
}
//debugf(DBG_LEVEL_NORM, 0,"Local time_t %li", mktime(&loc_time_tm));
return mktime(&loc_time_tm);
}
int get_time_as_string(time_t time_t_val, long nsec, char* time_str,
int time_str_len)
{
struct tm time_val_tm;
time_t safe_input_time;
//if time is incorrect (too long) you get segfault, need to check length and trim
if (time_t_val > INT_MAX)
{
debugf(DBG_LEVEL_NORM,
KRED"get_time_as_string: input time length too long, %lu > max=%lu, trimming!",
time_t_val, INT_MAX);
safe_input_time = 0;//(int)time_t_val;
}
else
safe_input_time = time_t_val;
time_val_tm = *gmtime(&safe_input_time);
int str_len = strftime(time_str, time_str_len, HUBIC_DATE_FORMAT,
&time_val_tm);
char nsec_str[TIME_CHARS];
sprintf(nsec_str, "%d", nsec);
strcat(time_str, nsec_str);
return str_len + strlen(nsec_str);
}
time_t get_time_now()
{
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
return now.tv_sec;
}
size_t get_time_now_as_str(char* time_str, int time_str_len)
{
time_t now = time(0);
struct tm tstruct;
tstruct = *localtime(&now);
// Visit http://en.cppreference.com/w/cpp/chrono/c/strftime
// for more information about date/time format
size_t result = strftime(time_str, time_str_len, HUBIC_DATE_FORMAT, &tstruct);
return result;
}
int get_timespec_as_str(const struct timespec* times, char* time_str,
int time_str_len)
{
return get_time_as_string(times->tv_sec, times->tv_nsec, time_str,
time_str_len);
}
char* str2md5(const char* str, int length)
{
int n;
MD5_CTX c;
unsigned char digest[16];
char* out = (char*)malloc(33);
MD5_Init(&c);
while (length > 0)
{
if (length > 512)
MD5_Update(&c, str, 512);
else
MD5_Update(&c, str, length);
length -= 512;
str += 512;
}
MD5_Final(digest, &c);
for (n = 0; n < 16; ++n)
snprintf(&(out[n * 2]), 16 * 2, "%02x", (unsigned int)digest[n]);
return out;
}
// http://stackoverflow.com/questions/10324611/how-to-calculate-the-md5-hash-of-a-large-file-in-c
int file_md5(FILE* file_handle, char* md5_file_str)
{
if (file_handle == NULL)
{
debugf(DBG_LEVEL_NORM, KRED"file_md5: NULL file handle");
return 0;
}
unsigned char c[MD5_DIGEST_LENGTH];
int i;
MD5_CTX mdContext;
int bytes;
char mdchar[3];//2 chars for md5 + null string terminator
unsigned char* data_buf = malloc(1024 * sizeof(unsigned char));
MD5_Init(&mdContext);
while ((bytes = fread(data_buf, 1, 1024, file_handle)) != 0)
MD5_Update(&mdContext, data_buf, bytes);
MD5_Final(c, &mdContext);
for (i = 0; i < MD5_DIGEST_LENGTH; i++)
{
snprintf(mdchar, 3, "%02x", c[i]);
strcat(md5_file_str, mdchar);
}
free(data_buf);
return 0;
}
int get_safe_cache_file_path(const char* path, char* file_path_safe,
char* temp_dir)
{
char tmp_path[PATH_MAX];
strncpy(tmp_path, path, PATH_MAX);
char* pch;
while ((pch = strchr(tmp_path, '/')))
* pch = '.';
char file_path[PATH_MAX] = "";
//temp file name had process pid in it, removed as on restart files are left in cache (pid changes)
snprintf(file_path, PATH_MAX, TEMP_FILE_NAME_FORMAT, temp_dir, tmp_path);
//fixme check if sizeof or strlen is suitable
int file_path_len = sizeof(file_path);
//the file path name using this format can go beyond NAME_MAX size and will generate error on fopen
//solution: cap file length to NAME_MAX, use a prefix from original path for debug purposes and add md5 id
char* md5_path = str2md5(file_path, file_path_len);
int md5len = strlen(md5_path);
size_t safe_len_prefix = min(NAME_MAX - md5len, file_path_len);
strncpy(file_path_safe, file_path, safe_len_prefix);
strncpy(file_path_safe + safe_len_prefix, md5_path, md5len);
//sometimes above copy process produces longer strings that NAME_MAX, force a null terminated string
file_path_safe[safe_len_prefix + md5len - 1] = '\0';
free(md5_path);
return strlen(file_path_safe);
}
void get_file_path_from_fd(int fd, char* path, int size_path)
{
char proc_path[MAX_PATH_SIZE];
/* Read out the link to our file descriptor. */
sprintf(proc_path, "/proc/self/fd/%d", fd);
memset(path, 0, size_path);
readlink(proc_path, path, size_path - 1);
}
//for file descriptor debugging
void debug_print_flags(int flags)
{
int accmode, val;
accmode = flags & O_ACCMODE;
if (accmode == O_RDONLY) debugf(DBG_LEVEL_EXTALL, KYEL"read only");
else if (accmode == O_WRONLY) debugf(DBG_LEVEL_EXTALL, KYEL"write only");
else if (accmode == O_RDWR) debugf(DBG_LEVEL_EXTALL, KYEL"read write");
else debugf(DBG_LEVEL_EXT, KYEL"unknown access mode");
if (val & O_APPEND) debugf(DBG_LEVEL_EXTALL, KYEL", append");
if (val & O_NONBLOCK) debugf(DBG_LEVEL_EXTALL, KYEL", nonblocking");
#if !defined(_POSIX_SOURCE) && defined(O_SYNC)
if (val & O_SYNC) debugf(DBG_LEVEL_EXT, 0,
KRED ", synchronous writes");
#endif
}
//for file descriptor debugging
void debug_print_descriptor(struct fuse_file_info* info)
{
char file_path[MAX_PATH_SIZE];
get_file_path_from_fd(info->fh, file_path, sizeof(file_path));
debugf(DBG_LEVEL_EXT, KCYN "descriptor localfile=[%s] fd=%d", file_path,
info->fh);
debug_print_flags(info->flags);
}
void dir_for(const char* path, char* dir)
{
strncpy(dir, path, MAX_PATH_SIZE);
char* slash = strrchr(dir, '/');
if (slash)
*slash = '\0';
}
//prints cache content for debug purposes
void debug_list_cache_content()
{
return;//disabled
dir_cache* cw;
dir_entry* de;
for (cw = dcache; cw; cw = cw->next)
{
debugf(DBG_LEVEL_EXT, "LIST-CACHE: DIR[%s]", cw->path);
for (de = cw->entries; de; de = de->next)
debugf(DBG_LEVEL_EXT, "LIST-CACHE: FOLDER[%s]", de->full_name);
}
}
int delete_file(char* path)
{
debugf(DBG_LEVEL_NORM, KYEL"delete_file(%s)", path);
char file_path_safe[NAME_MAX] = "";
get_safe_cache_file_path(path, file_path_safe, temp_dir);
int result = unlink(file_path_safe);
debugf(DBG_LEVEL_EXT, KYEL"delete_file(%s) (%s) result=%s", path,
file_path_safe, strerror(result));
return result;
}
//adding a directory in cache
dir_cache* new_cache(const char* path)
{
debugf(DBG_LEVEL_NORM, KCYN"new_cache(%s)", path);
dir_cache* cw = (dir_cache*)calloc(sizeof(dir_cache), 1);
cw->path = strdup(path);
cw->prev = NULL;
cw->entries = NULL;
cw->cached = time(NULL);
//added cache by access
cw->accessed_in_cache = time(NULL);
cw->was_deleted = false;
if (dcache)
dcache->prev = cw;
cw->next = dcache;
dir_cache* result;
result = (dcache = cw);
debugf(DBG_LEVEL_EXT, "exit: new_cache(%s)", path);
return result;
}
//todo: check if the program behaves ok when free_dir
//is made on a folder that has an operation in progress
void cloudfs_free_dir_list(dir_entry* dir_list)
{
//check for NULL as dir might be already removed from cache by other thread
debugf(DBG_LEVEL_NORM, "cloudfs_free_dir_list(%s)", dir_list->full_name);
while (dir_list)
{
dir_entry* de = dir_list;
dir_list = dir_list->next;
//remove file from disk cache, fix for issue #89, https://github.com/TurboGit/hubicfuse/issues/89
delete_file(de->full_name);
free(de->name);
free(de->full_name);
free(de->content_type);
//TODO free all added fields
free(de->md5sum);
free(de);
}
}
void dir_decache(const char* path)
{
dir_cache* cw;
debugf(DBG_LEVEL_NORM, "dir_decache(%s)", path);
pthread_mutex_lock(&dcachemut);
dir_entry* de, *tmpde;
char dir[MAX_PATH_SIZE];
dir_for(path, dir);
for (cw = dcache; cw; cw = cw->next)
{
debugf(DBG_LEVEL_EXT, "dir_decache: parse(%s)", cw->path);
if (!strcmp(cw->path, path))
{
if (cw == dcache)
dcache = cw->next;
if (cw->prev)
cw->prev->next = cw->next;
if (cw->next)
cw->next->prev = cw->prev;
debugf(DBG_LEVEL_EXT, "dir_decache: free_dir1(%s)", cw->path);
//fixme: this sometimes is NULL and generates segfaults, checking first
if (cw->entries != NULL)
cloudfs_free_dir_list(cw->entries);
free(cw->path);
free(cw);
}
else if (cw->entries && !strcmp(dir, cw->path))
{
if (!strcmp(cw->entries->full_name, path))
{
de = cw->entries;
cw->entries = de->next;
de->next = NULL;
debugf(DBG_LEVEL_EXT, "dir_decache: free_dir2()");
cloudfs_free_dir_list(de);
}
else for (de = cw->entries; de->next; de = de->next)
{
if (!strcmp(de->next->full_name, path))
{
tmpde = de->next;
de->next = de->next->next;
tmpde->next = NULL;
debugf(DBG_LEVEL_EXT, "dir_decache: free_dir3()", cw->path);
cloudfs_free_dir_list(tmpde);
break;
}
}
}
}
pthread_mutex_unlock(&dcachemut);
}
dir_entry* init_dir_entry()
{
dir_entry* de = (dir_entry*)malloc(sizeof(dir_entry));
de->metadata_downloaded = false;
de->size = 0;
de->next = NULL;
de->md5sum = NULL;
de->accessed_in_cache = time(NULL);
de->last_modified = time(NULL);
de->mtime.tv_sec = time(NULL);
de->atime.tv_sec = time(NULL);
de->ctime.tv_sec = time(NULL);
de->mtime.tv_nsec = 0;
de->atime.tv_nsec = 0;
de->ctime.tv_nsec = 0;
de->chmod = 0;
de->gid = 0;
de->uid = 0;
return de;
}
void copy_dir_entry(dir_entry* src, dir_entry* dst)
{
dst->atime.tv_sec = src->atime.tv_sec;
dst->atime.tv_nsec = src->atime.tv_nsec;
dst->mtime.tv_sec = src->mtime.tv_sec;
dst->mtime.tv_nsec = src->mtime.tv_nsec;
dst->ctime.tv_sec = src->ctime.tv_sec;
dst->ctime.tv_nsec = src->ctime.tv_nsec;
dst->chmod = src->chmod;
//todo: copy md5sum as well
}
//check for file in cache, if found size will be updated, if not found
//and this is a dir, a new dir cache entry is created
void update_dir_cache(const char* path, off_t size, int isdir, int islink)
{
debugf(DBG_LEVEL_EXTALL, KCYN "update_dir_cache(%s)", path);
pthread_mutex_lock(&dcachemut);
dir_cache* cw;
dir_entry* de;
char dir[MAX_PATH_SIZE];
dir_for(path, dir);
for (cw = dcache; cw; cw = cw->next)
{
if (!strcmp(cw->path, dir))
{
for (de = cw->entries; de; de = de->next)
{
if (!strcmp(de->full_name, path))
{
de->size = size;
pthread_mutex_unlock(&dcachemut);
debugf(DBG_LEVEL_EXTALL, "exit 0: update_dir_cache(%s)", path);
return;
}
}
de = init_dir_entry();
de->size = size;
de->isdir = isdir;
de->islink = islink;
de->name = strdup(&path[strlen(cw->path) + 1]);
de->full_name = strdup(path);
//fixed: the conditions below were mixed up dir -> link?
if (islink)
de->content_type = strdup("application/link");
if (isdir)
de->content_type = strdup("application/directory");
else
de->content_type = strdup("application/octet-stream");
de->next = cw->entries;
cw->entries = de;
if (isdir)
new_cache(path);
break;
}
}
debugf(DBG_LEVEL_EXTALL, "exit 1: update_dir_cache(%s)", path);
pthread_mutex_unlock(&dcachemut);
}
//returns first file entry in linked list. if not in cache will be downloaded.
int caching_list_directory(const char* path, dir_entry** list)
{
debugf(DBG_LEVEL_EXT, "caching_list_directory(%s)", path);
pthread_mutex_lock(&dcachemut);
bool new_entry = false;
if (!strcmp(path, "/"))
path = "";
dir_cache* cw;
for (cw = dcache; cw; cw = cw->next)
{
if (cw->was_deleted == true)
{
debugf(DBG_LEVEL_EXT,
KMAG"caching_list_directory status: dir(%s) is empty as cached expired, reload from cloud",
cw->path);
if (!cloudfs_list_directory(cw->path, list))
debugf(DBG_LEVEL_EXT,
KMAG"caching_list_directory status: cannot reload dir(%s)", cw->path);
else
{
debugf(DBG_LEVEL_EXT, KMAG"caching_list_directory status: reloaded dir(%s)",
cw->path);
//cw->entries = *list;
cw->was_deleted = false;
cw->cached = time(NULL);
}
}
if (cw->was_deleted == false)
{
if (!strcmp(cw->path, path))
break;
}
}
if (!cw)
{
//trying to download this entry from cloud, list will point to cached or downloaded entries
if (!cloudfs_list_directory(path, list))
{
//download was not ok
pthread_mutex_unlock(&dcachemut);
debugf(DBG_LEVEL_EXT,
"exit 0: caching_list_directory(%s) "KYEL"[CACHE-DIR-MISS]", path);
return 0;
}
debugf(DBG_LEVEL_EXT,
"caching_list_directory: new_cache(%s) "KYEL"[CACHE-CREATE]", path);
cw = new_cache(path);
new_entry = true;
}
else if (cache_timeout > 0 && (time(NULL) - cw->cached > cache_timeout))
{
if (!cloudfs_list_directory(path, list))
{
//mutex unlock was forgotten
pthread_mutex_unlock(&dcachemut);
debugf(DBG_LEVEL_EXT, "exit 1: caching_list_directory(%s)", path);
return 0;
}
//fixme: this frees dir subentries but leaves the dir parent entry, this confuses path_info
//which believes this dir has no entries
if (cw->entries != NULL)
{
cloudfs_free_dir_list(cw->entries);
cw->was_deleted = true;
cw->cached = time(NULL);
debugf(DBG_LEVEL_EXT, "caching_list_directory(%s) "KYEL"[CACHE-EXPIRED]",
path);
}
else
{
debugf(DBG_LEVEL_EXT,
"got NULL on caching_list_directory(%s) "KYEL"[CACHE-EXPIRED w NULL]", path);
pthread_mutex_unlock(&dcachemut);
return 0;
}
}
else
{
debugf(DBG_LEVEL_EXT, "caching_list_directory(%s) "KGRN"[CACHE-DIR-HIT]",
path);
*list = cw->entries;
}
//adding new dir file list to global cache, now this dir becomes visible in cache
cw->entries = *list;
pthread_mutex_unlock(&dcachemut);
debugf(DBG_LEVEL_EXT, "exit 2: caching_list_directory(%s)", path);
return 1;
}
dir_entry* path_info(const char* path)
{
debugf(DBG_LEVEL_EXT, "path_info(%s)", path);
char dir[MAX_PATH_SIZE];
dir_for(path, dir);
dir_entry* tmp;
if (!caching_list_directory(dir, &tmp))
{
debugf(DBG_LEVEL_EXT, "exit 0: path_info(%s) "KYEL"[CACHE-DIR-MISS]", dir);
return NULL;
}
else
debugf(DBG_LEVEL_EXT, "path_info(%s) "KGRN"[CACHE-DIR-HIT]", dir);
//iterate in file list obtained from cache or downloaded
for (; tmp; tmp = tmp->next)
{
if (!strcmp(tmp->full_name, path))
{
debugf(DBG_LEVEL_EXT, "exit 1: path_info(%s) "KGRN"[CACHE-FILE-HIT]", path);
return tmp;
}
}
//miss in case the file is not found on a cached folder
debugf(DBG_LEVEL_EXT, "exit 2: path_info(%s) "KYEL"[CACHE-MISS]", path);
return NULL;
}
//retrieve folder from local cache if exists, return null if does not exist (rather than download)
int check_caching_list_directory(const char* path, dir_entry** list)
{
debugf(DBG_LEVEL_EXT, "check_caching_list_directory(%s)", path);
pthread_mutex_lock(&dcachemut);
if (!strcmp(path, "/"))
path = "";
dir_cache* cw;
for (cw = dcache; cw; cw = cw->next)
if (!strcmp(cw->path, path))
{
*list = cw->entries;
pthread_mutex_unlock(&dcachemut);
debugf(DBG_LEVEL_EXT,
"exit 0: check_caching_list_directory(%s) "KGRN"[CACHE-DIR-HIT]", path);
return 1;
}
pthread_mutex_unlock(&dcachemut);
debugf(DBG_LEVEL_EXT,
"exit 1: check_caching_list_directory(%s) "KYEL"[CACHE-DIR-MISS]", path);
return 0;
}
dir_entry* check_parent_folder_for_file(const char* path)
{
char dir[MAX_PATH_SIZE];
dir_for(path, dir);
dir_entry* tmp;
if (!check_caching_list_directory(dir, &tmp))
return NULL;
else
return tmp;
}
//check if local path is in cache, without downloading from cloud if not in cache
dir_entry* check_path_info(const char* path)
{
debugf(DBG_LEVEL_EXT, "check_path_info(%s)", path);
char dir[MAX_PATH_SIZE];
dir_for(path, dir);
dir_entry* tmp;
//get parent folder cache entry
if (!check_caching_list_directory(dir, &tmp))
{
debugf(DBG_LEVEL_EXT, "exit 0: check_path_info(%s) "KYEL"[CACHE-MISS]", path);
return NULL;
}
for (; tmp; tmp = tmp->next)
{
if (!strcmp(tmp->full_name, path))
{
debugf(DBG_LEVEL_EXT, "exit 1: check_path_info(%s) "KGRN"[CACHE-HIT]", path);
return tmp;
}
}
if (!strcmp(path, "/"))
debugf(DBG_LEVEL_EXT,
"exit 2: check_path_info(%s) "KYEL"ignoring root [CACHE-MISS]", path);
else
debugf(DBG_LEVEL_EXT, "exit 3: check_path_info(%s) "KYEL"[CACHE-MISS]", path);
return NULL;
}
char* get_home_dir()
{
char* home;
if ((home = getenv("HOME")) && !access(home, R_OK))
return home;
struct passwd* pwd = getpwuid(geteuid());
if ((home = pwd->pw_dir) && !access(home, R_OK))
return home;
return "~";
}
void cloudfs_debug(int dbg)
{
debug = dbg;
}
void debugf(int level, char* fmt, ...)
{
if (debug)
{
if (level <= option_debug_level)
{
#ifdef SYS_gettid
pid_t thread_id = syscall(SYS_gettid);
#else
int thread_id = 0;
#error "SYS_gettid unavailable on this system"
#endif
va_list args;
char prefix[] = "==DBG %d [%s]:%d==";
char line[4096];
char time_str[TIME_CHARS];
get_time_now_as_str(time_str, sizeof(time_str));
sprintf(line, prefix, level, time_str, thread_id);
fputs(line, stderr);
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
fputs(KNRM, stderr);
putc('\n', stderr);
putc('\r', stderr);
}
}
}