efm2/src/backends/default/meta.c

444 lines
12 KiB
C

#include <Eina.h>
#include <Eet.h>
#include <Efreet.h>
#include <Ecore.h>
#include <Ecore_File.h>
#include <fcntl.h>
#include "eina_types.h"
#include "sha.h"
#include "meta.h"
// This is a metadata store to store added metadata for any given file path.
// For example store the x,y location of a file, added comments, flags a user
// may set on a file.
//
// "primary" location for medata for a /path/to/dir/ location is
// /path/to/dir/.efm/filename.efm where that file is a desktop style
// ini format with X-Efm-XXX=YYY fields where XXX is the meta key and YYY is
// the meta data. Secondary location is ~/.e/e/efm/meta/7a/172c003a7fb98e.efm
// i.e. the sha1 of the full target file path for metadata with first 2 hex
// chars being a toplevel dir to store all sub-hashes in that dir from the
// first 2 chars of the hex string of the sha1.
//
// if user cannot write to the primary location, metadata will be written to
// the secondary personal store location instead.
//
// this is not the most efficient in disk space or speed etc. but it
// "accessible" as text files on disk able to be edited and as this is
// the filemanager and is now doing things in a way to improve accessability
// to allow scripts, tools etc. to work with and generate data etc. then
// this is the compromise choice taken.
#define META_WRITE_TIMEOUT 0.2
typedef struct _Meta_File
{
const char *path; // path of original file this metadata is for
Eina_List *list; // for a small mount of meta - list of Meta
Eina_Bool changed : 1; // has changes to write out
} Meta_File;
typedef struct _Meta
{
const char *meta; // meta key
const char *data; // data in meta
} Meta;
static const char *_config_dir = NULL;
static Eina_Hash *_meta_hash = NULL;
static Eina_List *_meta_writes = NULL;
static Ecore_Timer *_meta_flush_timer = NULL;
///////////////////////////////////////////////////////////////////////////////
static char *
_meta_personal_overlay_file_path_get(const char *path, const char *extn)
{
unsigned char dst[20];
char sha1[41], buf[PATH_MAX];
eina_sha1((unsigned char *)path, strlen(path), dst);
sha1_str(dst, sha1);
snprintf(buf, sizeof(buf), "%s/efm/meta/%c%c/%s.%s",
_config_dir, sha1[0], sha1[1], sha1 + 2, extn);
return strdup(buf);
}
static char *
_meta_personal_overlay_file_get(Meta_File *mf)
{ // get secondary personal path to meta file for target path
return _meta_personal_overlay_file_path_get(mf->path, "meta.efm");
}
static char *
_meta_file_path_get(const char *path, const char *extn)
{ // get primary meta file for the target path
char *dir = ecore_file_dir_get(path);
char buf[PATH_MAX];
if (!dir) return NULL;
snprintf(buf, sizeof(buf), "%s/.efm/%s.%s",
dir, ecore_file_file_get(path), extn);
free(dir);
return strdup(buf);
}
static char *
_meta_file_get(Meta_File *mf)
{
return _meta_file_path_get(mf->path, "meta.efm");
}
static void
_meta_free(Meta *m)
{
if (m->meta) eina_stringshare_del(m->meta);
if (m->data) eina_stringshare_del(m->data);
free(m);
}
static void
_meta_file_free(Meta_File *mf)
{
Meta *m;
if (mf->path) eina_stringshare_del(mf->path);
EINA_LIST_FREE(mf->list, m) _meta_free(m);
free(mf);
}
static void
_meta_file_write(Meta_File *mf)
{ // write out all in memory metadata to target meta file
Eina_List *l;
Meta *m;
FILE *f;
char *meta_path = NULL, *dir = NULL;
if (!mf->changed) return;
mf->changed = EINA_FALSE;
meta_path = _meta_file_get(mf);
if (!meta_path) goto err;
dir = ecore_file_dir_get(meta_path);
if (!dir) goto err;
ecore_file_mkpath(dir);
free(dir);
dir = NULL;
if (mf->list)
{
// XXX: should gain a lock!
f = fopen(meta_path, "w");
if (!f)
{ // can't write to dir - write to personal meta instead
free(meta_path);
meta_path = _meta_personal_overlay_file_get(mf);
if (!meta_path) return;
dir = ecore_file_dir_get(meta_path);
if (!dir) return;
ecore_file_mkpath(dir);
free(dir);
dir = NULL;
f = fopen(meta_path, "w");
if (!f) goto err;
}
fprintf(f, "[Efm Meta]\n");
EINA_LIST_FOREACH(mf->list, l, m)
fprintf(f, "%s=%s\n", m->meta, m->data);
fclose(f);
}
else // no meta keys - delete it
ecore_file_unlink(meta_path);
err:
free(meta_path);
free(dir);
}
static void
_meta_writes_flush(void)
{ // flush all pending writes
Meta_File *mf;
// XXX: time this and don't spend more than X time writing out meta files
EINA_LIST_FREE(_meta_writes, mf) _meta_file_write(mf);
}
static Eina_Bool
_cb_meta_flush_timer(void *data EINA_UNUSED)
{
_meta_flush_timer = NULL;
_meta_writes_flush();
// XXX: if flush took too long - queue another timer and try flush again
// in future. repeate until nothing left to flush
return EINA_FALSE;
}
static void
_meta_file_write_queue(Meta_File *mf)
{ // tag the meta file as changed and queue some writes
if (_meta_flush_timer) ecore_timer_reset(_meta_flush_timer);
else _meta_flush_timer = ecore_timer_add(META_WRITE_TIMEOUT,
_cb_meta_flush_timer,
NULL);
if (!mf->changed)
{
_meta_writes = eina_list_append(_meta_writes, mf);
mf->changed = EINA_TRUE;
}
}
static void
_meta_hash_entry_free(void *data)
{
_meta_file_free(data);
}
static Eina_Bool
_cb_meta_desktop_x_foreach(const Eina_Hash *hash EINA_UNUSED, const void *key,
void *data, void *fdata)
{
Meta_File *mf = fdata;
Meta *m;
m = calloc(1, sizeof(Meta));
if (m)
{
m->meta = eina_stringshare_add(key /* + 6*/);
m->data = eina_stringshare_add(data);
mf->list = eina_list_append(mf->list, m);
}
return EINA_TRUE;
}
static Eina_Bool
_cb_meta_desktop_x_foreach_over(const Eina_Hash *hash EINA_UNUSED,
const void *key, void *data, void *fdata)
{
Meta_File *mf = fdata;
Meta *m;
Eina_List *l;
EINA_LIST_FOREACH(mf->list, l, m)
{ // find existing meta key and replace
if (!strcmp(m->meta, key /* + 6*/))
{
eina_stringshare_replace(&(m->data), data);
goto done;
}
}
// not found - append meta
m = calloc(1, sizeof(Meta));
if (m)
{
m->meta = eina_stringshare_add(key /* + 6*/);
m->data = eina_stringshare_add(data);
mf->list = eina_list_append(mf->list, m);
}
done:
return EINA_TRUE;
}
static Meta_File *
_meta_file_find(const char *path)
{
Meta_File *mf;
Efreet_Ini *ini;
char *meta_path;
// find existing in memory meta file data and return that
mf = eina_hash_find(_meta_hash, path);
if (mf) return mf;
// not found - alloc and load in from disk
mf = calloc(1, sizeof(Meta_File));
if (!mf) return NULL;
mf->path = eina_stringshare_add(path);
// load meta file data in dir itself first
meta_path = _meta_file_get(mf);
if (!meta_path) goto err;
ini = efreet_ini_new(meta_path);
if (ini)
{
if ((ini->data) && (efreet_ini_section_set(ini, "Efm Meta")))
eina_hash_foreach(ini->section, _cb_meta_desktop_x_foreach, mf);
efreet_ini_free(ini);
}
free(meta_path);
// load overlayed user metdata and modify meta file content based on it
meta_path = _meta_personal_overlay_file_get(mf);
if (meta_path)
{
ini = efreet_ini_new(meta_path);
if (ini)
{
if ((ini->data) && (efreet_ini_section_set(ini, "Efm Meta")))
eina_hash_foreach(ini->section, _cb_meta_desktop_x_foreach_over,
mf);
efreet_ini_free(ini);
}
free(meta_path);
}
// add to our hash db of meta files
eina_hash_add(_meta_hash, mf->path, mf);
return mf;
err:
if (mf->path) eina_stringshare_del(mf->path);
free(mf);
return NULL;
}
///////////////////////////////////////////////////////////////////////////////
void
meta_init(const char *config_dir)
{
_config_dir = eina_stringshare_add(config_dir);
_meta_hash = eina_hash_string_superfast_new(_meta_hash_entry_free);
}
void
meta_shutdown(void)
{
if (_meta_flush_timer)
{
ecore_timer_del(_meta_flush_timer);
_meta_writes_flush();
}
eina_hash_free(_meta_hash);
eina_stringshare_del(_config_dir);
}
void
meta_set(const char *path, const char *meta, const char *data)
{ // set meta data key "meta" on ffile "Path", data = NULL -> delete meta
Meta_File *mf = _meta_file_find(path);
Eina_List *l;
Meta *m;
if (!mf) return;
EINA_LIST_FOREACH(mf->list, l, m)
{
if (!strcmp(m->meta, meta))
{ // found matching meta key -> modify or del it
if (!strcmp(m->data, data)) return;
if (data) // modify it if data != NULL
eina_stringshare_replace(&m->data, data);
else
{ // NULL data - delete it
mf->list = eina_list_remove_list(mf->list, l);
_meta_free(m);
}
_meta_file_write_queue(mf); // queue writes for later
return;
}
}
if (!data) return; // data is null - we removed the meta
m = calloc(1, sizeof(Meta));
if (!m) return;
m->meta = eina_stringshare_add(meta);
m->data = eina_stringshare_add(data);
mf->list = eina_list_append(mf->list, m);
_meta_file_write_queue(mf); // queue writes for later
}
Eina_Stringshare *
meta_get(const char *path, const char *meta)
{ // get meta data key "meta" associated with file "path"
Meta_File *mf = _meta_file_find(path);
Eina_List *l;
Meta *m;
if (!mf) return NULL;
EINA_LIST_FOREACH(mf->list, l, m)
{
if (!strcmp(m->meta, meta)) return eina_stringshare_ref(m->data);
}
return NULL;
}
char *
meta_path_find(const char *path, const char *extn)
{ // "path" is the file path to get the filesystem local meta file for
return _meta_file_path_get(path, extn);
}
char *
meta_path_user_find(const char *path, const char *extn)
{ // "path" is the target file path to get the user overlay file for
return _meta_personal_overlay_file_path_get(path, extn);
}
Eina_Bool
meta_path_prepare(const char *path)
{ // "path" is the path returned by meta_path_user_find() or meta_path_find()
Eina_Bool ret = EINA_FALSE;
char *dir = ecore_file_dir_get(path);
if (!dir) return ret;
if (ecore_file_is_dir(dir)) ret = EINA_TRUE;
else
{
if (ecore_file_mkdir(dir)) ret = EINA_TRUE;
else if (ecore_file_mkpath(dir)) ret = EINA_TRUE;
}
free(dir);
return ret;
}
Eina_Bool
meta_path_can_write(const char *path)
{ // can we write to the taget dir of "path" file for meta data?
// yes - this is racey. but then again anything is. we could make a
// .efm dir then have permission removed before we make the subdiors
// or meta files. anything that involves more than a single file and
// a single open is going to suffer - deal with it. as this is expensive
// and modifies the fs just to test it it is a good idea to only call this
// once when opening a dir (or doing something that may write to it)
char buf[PATH_MAX];
struct stat st;
Eina_Bool ret = EINA_FALSE;
int res = 0;
char *dir = ecore_file_dir_get(path);
uid_t uid = getuid();
if (!dir) return ret;
if (stat(dir, &st) == -1) goto err;
// policy - we only cosider direst owned by the user writable. want to
// avoid e.g. root browsing then modifying dirs owne by a user or dirs
// that might have group write access being written to by multiple users
if ((st.st_uid == uid) && (st.st_mode & S_IWUSR))
{
snprintf(buf, sizeof(buf), "%s/.efm", dir);
res = mkdir(buf, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
if (res == 0)
{ // we created the dir - remove it (if not empty) - might not need
rmdir(buf);
ret = EINA_TRUE;
}
else
{
if (errno == EEXIST)
{ // dir exists - test we can create a file in it
snprintf(buf, sizeof(buf), "%s/.efm/.t", dir);
res = open(buf, O_WRONLY | O_CREAT | O_CLOEXEC);
if (res >= 0)
{ // we can create and write to file - remove it now
unlink(buf);
close(res);
ret = EINA_TRUE;
}
}
}
}
err:
free(dir);
return ret;
}