444 lines
12 KiB
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;
|
|
}
|