mirror of
https://github.com/owntracks/recorder.git
synced 2024-11-15 09:58:40 -07:00
e2d1359e4b
addresses #444
1774 lines
44 KiB
C
1774 lines
44 KiB
C
/*
|
|
* OwnTracks Recorder
|
|
* Copyright (C) 2015-2024 Jan-Piet Mens <jpmens@gmail.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* 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. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#define __USE_XOPEN 1
|
|
#define _GNU_SOURCE 1
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fnmatch.h>
|
|
#include <unistd.h>
|
|
#include <glob.h>
|
|
#include <ctype.h>
|
|
#include <assert.h>
|
|
#include <sys/stat.h>
|
|
#include "utstring.h"
|
|
#include "storage.h"
|
|
#include "geohash.h"
|
|
#include "fences.h"
|
|
#include "gcache.h"
|
|
#include "util.h"
|
|
#include "listsort.h"
|
|
|
|
char STORAGEDIR[BUFSIZ] = STORAGEDEFAULT;
|
|
|
|
#define LINESIZE 8192
|
|
#define LARGEBUF (BUFSIZ * 2)
|
|
|
|
static struct gcache *gc = NULL;
|
|
|
|
#ifdef WITH_TZ
|
|
# include "zonedetect.h"
|
|
static ZoneDetect *zdb = NULL;
|
|
#endif
|
|
|
|
void storage_init(int revgeo)
|
|
{
|
|
setenv("TZ", "UTC", 1);
|
|
|
|
if (revgeo) {
|
|
char path[LARGEBUF];
|
|
|
|
snprintf(path, LARGEBUF, "%s/ghash", STORAGEDIR);
|
|
gc = gcache_open(path, NULL, TRUE);
|
|
if (gc == NULL) {
|
|
olog(LOG_ERR, "storage_init(): gc is NULL");
|
|
}
|
|
}
|
|
|
|
#ifdef WITH_TZ
|
|
if (zdb == NULL) {
|
|
zdb = ZDOpenDatabase(TZDATADB);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void storage_gcache_dump(char *lmdbname)
|
|
{
|
|
char path[LARGEBUF];
|
|
snprintf(path, LARGEBUF, "%s/ghash", STORAGEDIR);
|
|
|
|
gcache_dump(path, lmdbname);
|
|
}
|
|
|
|
void storage_gcache_load(char *lmdbname)
|
|
{
|
|
char path[LARGEBUF];
|
|
|
|
snprintf(path, LARGEBUF, "%s/ghash", STORAGEDIR);
|
|
gcache_load(path, lmdbname);
|
|
}
|
|
|
|
/*
|
|
* Adjust seconds obtained via mktime() to offset from the last
|
|
* timezone we set. This fixes time to what GMT time is without
|
|
* actually knowing what the local $TZ is. We do this to avoid
|
|
* switching $TZ back and forth from UTC to whatever tzname
|
|
* a location has, a very expensive operation.
|
|
*/
|
|
|
|
time_t local2gmt(time_t tlocal) {
|
|
struct tm *tm;
|
|
time_t gm_tics;
|
|
|
|
tm = localtime(&tlocal);
|
|
gm_tics = tlocal + tm->tm_gmtoff;
|
|
|
|
return gm_tics;
|
|
}
|
|
|
|
static char * my_strptime(const char * restrict buf, const char * restrict format,
|
|
struct tm * restrict timeptr)
|
|
{
|
|
return strptime(buf, format, timeptr);
|
|
}
|
|
|
|
void get_geo(JsonNode *o, char *ghash)
|
|
{
|
|
JsonNode *geo;
|
|
|
|
if ((geo = gcache_json_get(gc, ghash)) != NULL) {
|
|
json_copy_to_object(o, geo, FALSE);
|
|
json_delete(geo);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Populate a JSON object (`obj') keyed by directory name; each element points
|
|
* to a JSON array with a list of subdirectories on the first level.
|
|
*
|
|
* { "jpm": [ "5s", "nex4" ], "jjolie": [ "iphone5" ] }
|
|
*
|
|
* Returns 1 on failure.
|
|
*/
|
|
|
|
static int user_device_list(char *name, int level, JsonNode *obj)
|
|
{
|
|
DIR *dirp;
|
|
struct dirent *dp;
|
|
char path[BUFSIZ];
|
|
JsonNode *devlist;
|
|
int rc = 0;
|
|
struct stat sb;
|
|
|
|
if ((dirp = opendir(name)) == NULL) {
|
|
perror(name);
|
|
return (1);
|
|
}
|
|
while ((dp = readdir(dirp)) != NULL) {
|
|
if (*dp->d_name != '.') {
|
|
sprintf(path, "%s/%s", name, dp->d_name);
|
|
|
|
if (stat(path, &sb) != 0) {
|
|
continue;
|
|
}
|
|
if (!S_ISDIR(sb.st_mode))
|
|
continue;
|
|
|
|
|
|
if (level == 0) {
|
|
devlist = json_mkarray();
|
|
json_append_member(obj, dp->d_name, devlist);
|
|
rc = user_device_list(path, level + 1, devlist);
|
|
} else if (level == 1) {
|
|
json_append_element(obj, json_mkstring(dp->d_name));
|
|
}
|
|
}
|
|
}
|
|
closedir(dirp);
|
|
return (rc);
|
|
}
|
|
|
|
#if WITH_GREENWICH
|
|
/*
|
|
* See if we have batt,ext,status data for this device, and if so, add
|
|
* their arrays into the JSON object at `last'.
|
|
*/
|
|
|
|
static void get_gw_data(char *username, char *device, JsonNode *last)
|
|
{
|
|
JsonNode *array;
|
|
static char *types[] = { "batt", "ext", "status", NULL };
|
|
char **t, *js;
|
|
static UT_string *ts = NULL, *u = NULL, *d = NULL;
|
|
|
|
if (last == NULL || last->tag != JSON_OBJECT)
|
|
return;
|
|
|
|
|
|
for (t = types; t && *t; t++) {
|
|
utstring_renew(u);
|
|
utstring_renew(d);
|
|
utstring_printf(u, "%s", username);
|
|
utstring_printf(d, "%s", device);
|
|
lowercase(UB(u));
|
|
lowercase(UB(d));
|
|
utstring_renew(ts);
|
|
utstring_printf(ts, "%s/last/%s/%s/%s.json",
|
|
STORAGEDIR,
|
|
UB(u),
|
|
UB(d),
|
|
*t);
|
|
|
|
/* Read file into JSON array and append to `last' object */
|
|
if ((js = slurp_file(UB(ts), TRUE)) != NULL) {
|
|
if ((array = json_decode(js)) != NULL) {
|
|
json_append_member(last, *t, array);
|
|
}
|
|
free(js);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif /* GREENWICH */
|
|
|
|
void append_card_to_object(JsonNode *obj, char *user, char *device)
|
|
{
|
|
char path[LARGEBUF], path1[LARGEBUF], *cardfile = NULL;
|
|
JsonNode *card;
|
|
|
|
if (!user || !*user)
|
|
return;
|
|
|
|
|
|
snprintf(path, LARGEBUF, "%s/cards/%s/%s/%s-%s.json",
|
|
STORAGEDIR, user, device, user, device);
|
|
if (access(path, R_OK) == 0) {
|
|
cardfile = path;
|
|
} else {
|
|
snprintf(path1, LARGEBUF, "%s/cards/%s/%s.json",
|
|
STORAGEDIR, user, user);
|
|
|
|
if (access(path1, R_OK) == 0) {
|
|
cardfile = path1;
|
|
}
|
|
}
|
|
|
|
card = json_mkobject();
|
|
if (cardfile && json_copy_from_file(card, cardfile) == TRUE) {
|
|
json_copy_to_object(obj, card, FALSE);
|
|
}
|
|
json_delete(card);
|
|
}
|
|
|
|
static void tz_info(JsonNode *json, double lat, double lon, time_t tst)
|
|
{
|
|
// olog(LOG_DEBUG, "tz_info for (%lf, %lf)", lat, lon);
|
|
if (zdb) {
|
|
char *tz_str = ZDHelperSimpleLookupString(zdb, lat, lon);
|
|
|
|
if (tz_str) {
|
|
json_append_member(json, "tzname", json_mkstring(tz_str));
|
|
json_append_member(json, "isolocal", json_mkstring(isolocal(tst, tz_str)));
|
|
|
|
ZDHelperSimpleLookupStringFree(tz_str);
|
|
}
|
|
}
|
|
}
|
|
|
|
void append_device_details(JsonNode *userlist, char *user, char *device)
|
|
{
|
|
char path[LARGEBUF];
|
|
JsonNode *node, *last;
|
|
|
|
snprintf(path, LARGEBUF, "%s/last/%s/%s/%s-%s.json",
|
|
STORAGEDIR, user, device, user, device);
|
|
|
|
last = json_mkobject();
|
|
if (json_copy_from_file(last, path) == TRUE) {
|
|
JsonNode *jtst, *jlat, *jlon;
|
|
|
|
if ((jtst = json_find_member(last, "tst")) != NULL) {
|
|
json_append_member(last, "isotst", json_mkstring(isotime(jtst->number_)));
|
|
json_append_member(last, "disptst", json_mkstring(disptime(jtst->number_)));
|
|
|
|
if ((jlat = json_find_member(last, "lat")) &&
|
|
(jlon = json_find_member(last, "lon"))) {
|
|
tz_info(last, jlat->number_, jlon->number_, jtst->number_);
|
|
}
|
|
}
|
|
}
|
|
|
|
append_card_to_object(last, user, device);
|
|
|
|
|
|
if ((node = json_find_member(last, "ghash")) != NULL) {
|
|
if (node->tag == JSON_STRING) {
|
|
get_geo(last, node->string_);
|
|
}
|
|
}
|
|
|
|
/* Extra data */
|
|
snprintf(path, LARGEBUF, "%s/last/%s/%s/extra.json",
|
|
STORAGEDIR, user, device);
|
|
json_copy_from_file(last, path);
|
|
#if WITH_GREENWICH
|
|
get_gw_data(user, device, last);
|
|
#endif
|
|
json_append_element(userlist, last);
|
|
}
|
|
|
|
/*
|
|
* Return an array of users gleaned from LAST with merged details.
|
|
* If user and device are specified, limit to those; either may be
|
|
* NULL. If `fields' is not NULL, it's a JSON array of fields to
|
|
* be returned.
|
|
*/
|
|
|
|
JsonNode *last_users(char *in_user, char *in_device, JsonNode *fields)
|
|
{
|
|
JsonNode *obj = json_mkobject();
|
|
JsonNode *un, *dn, *userlist = json_mkarray();
|
|
char path[LARGEBUF], user[BUFSIZ], device[BUFSIZ];
|
|
|
|
snprintf(path, LARGEBUF, "%s/last", STORAGEDIR);
|
|
|
|
// fprintf(stderr, "last_users(%s, %s)\n", (in_user) ? in_user : "<nil>",
|
|
// (in_device) ? in_device : "<nil>");
|
|
|
|
if (user_device_list(path, 0, obj) == 1) {
|
|
json_delete(userlist);
|
|
return (obj);
|
|
}
|
|
|
|
/* Loop through users, devices */
|
|
json_foreach(un, obj) {
|
|
if (un->tag != JSON_ARRAY)
|
|
continue;
|
|
strcpy(user, un->key);
|
|
json_foreach(dn, un) {
|
|
if (dn->tag == JSON_STRING) {
|
|
strcpy(device, dn->string_);
|
|
} else if (dn->tag == JSON_NUMBER) { /* all digits? */
|
|
sprintf(device, "%.lf", dn->number_);
|
|
}
|
|
|
|
if (!in_user && !in_device) {
|
|
append_device_details(userlist, user, device);
|
|
} else if (in_user && !in_device) {
|
|
if (strcmp(user, in_user) == 0) {
|
|
append_device_details(userlist, user, device);
|
|
}
|
|
} else if (in_user && in_device) {
|
|
if (strcmp(user, in_user) == 0 && strcmp(device, in_device) == 0) {
|
|
append_device_details(userlist, user, device);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
json_delete(obj);
|
|
|
|
/*
|
|
* userlist now is an array of user objects. If fields were
|
|
* specified, re-create that array with object which have
|
|
* only the requested fields. It's a shame we have to re-do
|
|
* this here, but I can't think of an alternative way at
|
|
* the moment.
|
|
*/
|
|
|
|
if (fields) {
|
|
JsonNode *new_userlist = json_mkarray(), *user;
|
|
|
|
json_foreach(user, userlist) {
|
|
JsonNode *o = json_mkobject(), *f, *j;
|
|
|
|
json_foreach(f, fields) {
|
|
char *field_name = f->string_;
|
|
|
|
if ((j = json_find_member(user, field_name)) != NULL) {
|
|
json_copy_element_to_object(o, field_name, j);
|
|
}
|
|
}
|
|
json_append_element(new_userlist, o);
|
|
}
|
|
|
|
|
|
json_delete(userlist);
|
|
return (new_userlist);
|
|
}
|
|
|
|
return (userlist);
|
|
}
|
|
|
|
/*
|
|
* `s' has a time string in it. Try to convert into time_t
|
|
* using a variety of formats from higher to lower precision.
|
|
* Return 1 on success, 0 on failure.
|
|
*/
|
|
|
|
static int str_time_to_secs(char *s, time_t *secs)
|
|
{
|
|
static char **f, *formats[] = {
|
|
"%Y-%m-%dT%H:%M:%S",
|
|
"%Y-%m-%dT%H:%M",
|
|
"%Y-%m-%dT%H",
|
|
"%Y-%m-%dt%H:%M:%S",
|
|
"%Y-%m-%dt%H:%M",
|
|
"%Y-%m-%dt%H",
|
|
"%Y-%m-%d",
|
|
"%Y-%m",
|
|
NULL
|
|
};
|
|
struct tm tm;
|
|
int success = 0;
|
|
|
|
memset(&tm, 0, sizeof(struct tm));
|
|
for (f = formats; f && *f; f++) {
|
|
if (my_strptime(s, *f, &tm) != NULL) {
|
|
success = 1;
|
|
// fprintf(stderr, "str_time_to_secs succeeds with %s\n", *f);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!success)
|
|
return (0);
|
|
|
|
tm.tm_mday = (tm.tm_mday < 1) ? 1 : tm.tm_mday;
|
|
tm.tm_isdst = -1; /* A negative value for tm_isdst causes
|
|
* the mktime() function to attempt to
|
|
* divine whether summer time is in
|
|
* effect for the specified time. */
|
|
|
|
*secs = local2gmt(mktime(&tm));
|
|
// fprintf(stderr, "str_time_to_secs: %s becomes %04d-%02d-%02d %02d:%02d:%02d\n",
|
|
// s,
|
|
// tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
|
// tm.tm_hour, tm.tm_min, tm.tm_sec);
|
|
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* For each of the strings, create an epoch time at the s_ pointers. If
|
|
* a string is NULL, use a default time (from: HOURS previous, to: now)
|
|
* Return 1 if the time conversion was successful and from <= to.
|
|
*/
|
|
|
|
int make_times(char *time_from, time_t *s_lo, char *time_to, time_t *s_hi, int hours)
|
|
{
|
|
time_t now;
|
|
|
|
time(&now);
|
|
if (!time_from || !*time_from) {
|
|
*s_lo = now - (60 * 60 * ((hours) ? hours : DEFAULT_HISTORY_HOURS));
|
|
} else {
|
|
if (str_time_to_secs(time_from, s_lo) == 0)
|
|
return (0);
|
|
}
|
|
|
|
if (!time_to || !*time_to) {
|
|
*s_hi = now;
|
|
} else {
|
|
if (str_time_to_secs(time_to, s_hi) == 0)
|
|
return (0);
|
|
}
|
|
return (*s_lo > *s_hi ? 0 : 1);
|
|
}
|
|
|
|
/*
|
|
* List the directories in the directory at `path' and put
|
|
* the names into a JSON array which is in obj.
|
|
*/
|
|
|
|
static void ls(char *path, JsonNode *obj)
|
|
{
|
|
DIR *dirp;
|
|
struct dirent *dp;
|
|
JsonNode *jarr = json_mkarray();
|
|
|
|
if (obj == NULL || obj->tag != JSON_OBJECT) {
|
|
json_delete(jarr);
|
|
return;
|
|
}
|
|
|
|
if ((dirp = opendir(path)) == NULL) {
|
|
json_append_member(obj, "error", json_mkstring("Cannot open requested directory"));
|
|
json_delete(jarr);
|
|
return;
|
|
}
|
|
|
|
while ((dp = readdir(dirp)) != NULL) {
|
|
bool is_dir = false;
|
|
|
|
/* XFS doesn't support d_type, and it's used on CentOS 7.
|
|
* Ensure we can determine whether something's a directory.
|
|
*/
|
|
|
|
if (dp->d_type == DT_DIR) {
|
|
is_dir = true;
|
|
} else {
|
|
struct stat st;
|
|
static UT_string *fullp = NULL;
|
|
|
|
utstring_renew(fullp);
|
|
utstring_printf(fullp, "%s/%s", path, dp->d_name);
|
|
|
|
assert(stat(UB(fullp), &st) != -1);
|
|
is_dir = S_ISDIR(st.st_mode);
|
|
}
|
|
if (is_dir && (*dp->d_name != '.')) {
|
|
json_append_element(jarr, json_mkstring(dp->d_name));
|
|
}
|
|
}
|
|
|
|
json_append_member(obj, "results", jarr);
|
|
closedir(dirp);
|
|
}
|
|
|
|
/*
|
|
* List the files in the directory at `pathpat' and
|
|
* put the names into a new JSON array in obj. Filenames (2015-08.rec)
|
|
* are checked whether they fall (time-wise) into the times between
|
|
* s_lo and s_hi.
|
|
*/
|
|
|
|
static time_t t_lo, t_hi; /* must be global so that filter() can access them */
|
|
|
|
static int filter_filename(const struct dirent *d)
|
|
{
|
|
struct tm tmfile, *tm;
|
|
int lo_months, hi_months, file_months;
|
|
|
|
/* if the filename doesn't look like YYYY-MM.rec we can safely ignore it.
|
|
* Needs modifying after the year 2999 ;-) */
|
|
if (fnmatch("2[0-9][0-9][0-9]-[0-3][0-9].rec", d->d_name, 0) != 0)
|
|
return (0);
|
|
|
|
/* Convert filename (YYYY-MM) to a tm; see if months falls between
|
|
* from months and to months. */
|
|
|
|
memset(&tmfile, 0, sizeof(struct tm));
|
|
if (my_strptime(d->d_name, "%Y-%m", &tmfile) == NULL) {
|
|
fprintf(stderr, "filter: convert err");
|
|
return (0);
|
|
}
|
|
file_months = (tmfile.tm_year + 1900) * 12 + tmfile.tm_mon;
|
|
|
|
tm = gmtime(&t_lo);
|
|
lo_months = (tm->tm_year + 1900) * 12 + tm->tm_mon;
|
|
|
|
tm = gmtime(&t_hi);
|
|
hi_months = (tm->tm_year + 1900) * 12 + tm->tm_mon;
|
|
|
|
/*
|
|
printf("filter: file %s has %04d-%02d-%02d %02d:%02d:%02d\n",
|
|
d->d_name,
|
|
tmfile.tm_year + 1900, tmfile.tm_mon + 1, tmfile.tm_mday,
|
|
tmfile.tm_hour, tmfile.tm_min, tmfile.tm_sec);
|
|
*/
|
|
|
|
if (file_months >= lo_months && file_months <= hi_months) {
|
|
// fprintf(stderr, "filter: returns: %s\n", d->d_name);
|
|
return (1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int cmp( const struct dirent **a, const struct dirent **b)
|
|
{
|
|
return strcmp((*a)->d_name, (*b)->d_name);
|
|
}
|
|
|
|
static void lsscan(char *pathpat, time_t s_lo, time_t s_hi, JsonNode *obj, int reverse)
|
|
{
|
|
struct dirent **namelist;
|
|
int i, n;
|
|
JsonNode *jarr = NULL;
|
|
static UT_string *path = NULL;
|
|
|
|
if (obj == NULL || obj->tag != JSON_OBJECT)
|
|
return;
|
|
|
|
|
|
utstring_renew(path);
|
|
|
|
/* Set global t_ values */
|
|
t_lo = s_lo;
|
|
t_hi = s_hi;
|
|
|
|
if ((n = scandir(pathpat, &namelist, filter_filename, cmp)) < 0) {
|
|
json_append_member(obj, "error", json_mkstring("Cannot lsscan requested directory"));
|
|
return;
|
|
}
|
|
|
|
/* If our obj contains the "results" array, use that
|
|
* and remove from obj; we'll add it back later.
|
|
*/
|
|
if ((jarr = json_find_member(obj, "results")) == NULL) {
|
|
jarr = json_mkarray();
|
|
} else {
|
|
json_delete(jarr);
|
|
}
|
|
|
|
if (reverse) {
|
|
for (i = n - 1; i >= 0; i--) {
|
|
utstring_clear(path);
|
|
utstring_printf(path, "%s/%s", pathpat, namelist[i]->d_name);
|
|
json_append_element(jarr, json_mkstring(UB(path)));
|
|
free(namelist[i]);
|
|
}
|
|
} else {
|
|
for (i = 0; i < n; i++) {
|
|
utstring_clear(path);
|
|
utstring_printf(path, "%s/%s", pathpat, namelist[i]->d_name);
|
|
json_append_element(jarr, json_mkstring(UB(path)));
|
|
free(namelist[i]);
|
|
}
|
|
}
|
|
free(namelist);
|
|
|
|
json_append_member(obj, "results", jarr);
|
|
}
|
|
|
|
/*
|
|
* If `user' and `device' are both NULL, return list of users.
|
|
* If `user` is specified, and device is NULL, return user's devices
|
|
* If both user and device are specified, return list of .rec files;
|
|
* in that case, limit with `s_lo` and `s_hi`. `reverse' is TRUE if
|
|
* list should be sorted in descending order.
|
|
*/
|
|
|
|
JsonNode *lister(char *user, char *device, time_t s_lo, time_t s_hi, int reverse)
|
|
{
|
|
JsonNode *json = json_mkobject();
|
|
static UT_string *path = NULL;
|
|
char *bp;
|
|
|
|
utstring_renew(path);
|
|
|
|
for (bp = user; bp && *bp; bp++) {
|
|
if (isupper(*bp))
|
|
*bp = tolower(*bp);
|
|
}
|
|
for (bp = device; bp && *bp; bp++) {
|
|
if (isupper(*bp))
|
|
*bp = tolower(*bp);
|
|
}
|
|
|
|
if (!user && !device) {
|
|
utstring_printf(path, "%s/rec", STORAGEDIR);
|
|
ls(UB(path), json);
|
|
} else if (!device) {
|
|
utstring_printf(path, "%s/rec/%s", STORAGEDIR, user);
|
|
ls(UB(path), json);
|
|
} else {
|
|
utstring_printf(path, "%s/rec/%s/%s",
|
|
STORAGEDIR, user, device);
|
|
lsscan(UB(path), s_lo, s_hi, json, reverse);
|
|
}
|
|
|
|
return (json);
|
|
}
|
|
|
|
/*
|
|
* List multiple user/device combinations. udpairs is a JSON
|
|
* array of strings, each of which is a user/device pair
|
|
* separated by a slash.
|
|
*/
|
|
|
|
JsonNode *multilister(JsonNode *udpairs, time_t s_lo, time_t s_hi, int reverse)
|
|
{
|
|
JsonNode *json = json_mkobject(), *ud;
|
|
static UT_string *path = NULL;
|
|
char *pairs[3];
|
|
int np;
|
|
|
|
if (udpairs == NULL || udpairs->tag != JSON_ARRAY) {
|
|
return (json);
|
|
}
|
|
|
|
json_foreach(ud, udpairs) {
|
|
if ((np = splitter(ud->string_, "/", pairs)) != 2) {
|
|
continue;
|
|
}
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/rec/%s/%s",
|
|
STORAGEDIR,
|
|
pairs[0], /* user */
|
|
pairs[1]); /* device */
|
|
lsscan(UB(path), s_lo, s_hi, json, reverse);
|
|
|
|
splitterfree(pairs);
|
|
}
|
|
|
|
return (json);
|
|
}
|
|
|
|
struct jparam {
|
|
JsonNode *obj;
|
|
JsonNode *locs;
|
|
time_t s_lo;
|
|
time_t s_hi;
|
|
output_type otype;
|
|
int limit; /* if non-zero, we're searching backwards */
|
|
JsonNode *fields; /* If non-NULL array of fields names to return */
|
|
char *username; /* If non-NULL, add username to location */
|
|
char *device; /* If non-NULL, add device name to location */
|
|
};
|
|
|
|
/*
|
|
* line is a single valid '*' line from a .rec file. Turn it into a JSON object.
|
|
* objectorize it. Is that a word? :)
|
|
*/
|
|
|
|
static JsonNode *line_to_location(char *line)
|
|
{
|
|
JsonNode *json, *o, *j;
|
|
char *ghash;
|
|
#ifdef WITH_TZ
|
|
char *tzname = NULL;
|
|
#endif
|
|
char tstamp[64], *bp;
|
|
double lat, lon;
|
|
long tst;
|
|
int geoprec = geohash_prec();
|
|
|
|
snprintf(tstamp, 21, "%s", line);
|
|
|
|
if ((bp = strchr(line, '{')) == NULL)
|
|
return (NULL);
|
|
|
|
if ((json = json_decode(bp)) == NULL) {
|
|
return (NULL);
|
|
}
|
|
|
|
if ((j = json_find_member(json, "_type")) == NULL) {
|
|
json_delete(json);
|
|
return (NULL);
|
|
}
|
|
if (j->tag != JSON_STRING || strcmp(j->string_, "location") != 0) {
|
|
json_delete(json);
|
|
return (NULL);
|
|
}
|
|
|
|
o = json_mkobject();
|
|
|
|
if (json_copy_to_object(o, json, FALSE) == FALSE) {
|
|
json_delete(o);
|
|
json_delete(json);
|
|
return (NULL);
|
|
}
|
|
json_delete(json); /* Done with this -- we've copied it. */
|
|
|
|
lat = lon = 0.0;
|
|
if ((j = json_find_member(o, "lat")) != NULL) {
|
|
lat = j->number_;
|
|
}
|
|
if ((j = json_find_member(o, "lon")) != NULL) {
|
|
lon = j->number_;
|
|
}
|
|
|
|
if ((j = json_find_member(o, "_geoprec")) != NULL) {
|
|
geoprec = j->number_;
|
|
}
|
|
|
|
if ((ghash = geohash_encode(lat, lon, abs(geoprec))) != NULL) {
|
|
json_append_member(o, "ghash", json_mkstring(ghash));
|
|
get_geo(o, ghash);
|
|
free(ghash);
|
|
}
|
|
|
|
json_append_member(o, "isorcv", json_mkstring(tstamp));
|
|
|
|
tst = 0L;
|
|
if ((j = json_find_member(o, "tst")) != NULL) {
|
|
tst = j->number_;
|
|
}
|
|
json_append_member(o, "isotst", json_mkstring(isotime(tst)));
|
|
json_append_member(o, "disptst", json_mkstring(disptime(tst)));
|
|
|
|
/*
|
|
* If tzname is in the cached geo data, we've already added it
|
|
* to the outgoing JSON and we use it to construct isolocal.
|
|
* Otherwise determine the TZ name from the tzdatadb.
|
|
*/
|
|
|
|
if ((j = json_find_member(o, "tzname")) != NULL) {
|
|
#ifdef WITH_TZ
|
|
tzname = j->string_;
|
|
json_append_member(o, "isolocal", json_mkstring(isolocal(tst, tzname)));
|
|
#endif
|
|
} else {
|
|
#ifdef WITH_TZ
|
|
tz_info(o, lat, lon, tst);
|
|
#endif
|
|
}
|
|
|
|
return (o);
|
|
}
|
|
|
|
/*
|
|
* Invoked via tac() and cat(). Verify that line is indeed a location
|
|
* line from a .rec file. Then objectorize it and add to the locations
|
|
* JSON array and update the counter in our JSON object.
|
|
* Return 0 to tell caller to "ignore" the line, 1 to use it.
|
|
*/
|
|
|
|
static int candidate_line(char *line, void *param)
|
|
{
|
|
long counter = 0L;
|
|
JsonNode *j, *o;
|
|
struct jparam *jarg = (struct jparam*)param;
|
|
char *bp;
|
|
JsonNode *obj = jarg->obj;
|
|
JsonNode *locs = jarg->locs;
|
|
int limit = jarg->limit;
|
|
time_t s_lo = jarg->s_lo;
|
|
time_t s_hi = jarg->s_hi;
|
|
JsonNode *fields = jarg->fields;
|
|
output_type otype = jarg->otype;
|
|
char *username = jarg->username;
|
|
char *device = jarg->device;
|
|
|
|
if (obj == NULL || obj->tag != JSON_OBJECT)
|
|
return (-1);
|
|
if (locs == NULL || locs->tag != JSON_ARRAY)
|
|
return (-1);
|
|
|
|
if (limit == 0) {
|
|
/* Reading forwards; account for time */
|
|
|
|
char *p;
|
|
struct tm tmline;
|
|
time_t secs;
|
|
|
|
if ((p = my_strptime(line, "%Y-%m-%dT%H:%M:%SZ", &tmline)) == NULL) {
|
|
fprintf(stderr, "invalid strptime format on %s", line);
|
|
return (0);
|
|
}
|
|
tmline.tm_isdst = -1; /* A negative value for tm_isdst causes
|
|
* the mktime() function to attempt to
|
|
* divine whether summer time is in
|
|
* effect for the specified time. */
|
|
secs = local2gmt(mktime(&tmline));
|
|
|
|
if (secs <= s_lo || secs >= s_hi) {
|
|
return (0);
|
|
}
|
|
|
|
if (otype == RAW) {
|
|
printf("%s\n", line);
|
|
return (0);
|
|
} else if (otype == RAWPAYLOAD) {
|
|
char *bp;
|
|
|
|
if ((bp = strchr(line, '{')) != NULL) {
|
|
printf("%s\n", bp);
|
|
}
|
|
}
|
|
|
|
} else if (limit > 0 && otype == RAW) {
|
|
printf("%s\n", line);
|
|
return (1); /* make it 'count' or tac() will not decrement line counter and continue until EOF */
|
|
} else if (limit > 0) {
|
|
/* reading backwards; check time */
|
|
|
|
char *p;
|
|
struct tm tmline;
|
|
time_t secs;
|
|
|
|
if ((p = my_strptime(line, "%Y-%m-%dT%H:%M:%SZ", &tmline)) == NULL) {
|
|
fprintf(stderr, "invalid strptime format on %s", line);
|
|
return (0);
|
|
}
|
|
tmline.tm_isdst = -1;
|
|
secs = local2gmt(mktime(&tmline));
|
|
|
|
if (secs <= s_lo || secs >= s_hi) {
|
|
return (0);
|
|
}
|
|
}
|
|
|
|
/* Do we have location line? */
|
|
if ((bp = strstr(line, "Z\t* ")) == NULL) { /* Not a location line */
|
|
return (0);
|
|
}
|
|
|
|
if ((bp = strrchr(bp, '\t')) == NULL) {
|
|
return (0);
|
|
}
|
|
|
|
/* Initialize our counter to what the JSON obj currently has */
|
|
if ((j = json_find_member(obj, "count")) != NULL) {
|
|
counter = j->number_;
|
|
json_delete(j);
|
|
}
|
|
|
|
// fprintf(stderr, "-->[%s]\n", line);
|
|
|
|
if ((o = line_to_location(line)) != NULL) {
|
|
|
|
/*
|
|
* Username/device are added typically for multilister() only.
|
|
*/
|
|
|
|
if (username)
|
|
json_append_member(o, "username", json_mkstring(username));
|
|
if (device)
|
|
json_append_member(o, "device", json_mkstring(device));
|
|
|
|
if (fields) {
|
|
/* Create a new object, copying members we're interested in into it */
|
|
JsonNode *f, *node;
|
|
JsonNode *newo = json_mkobject();
|
|
|
|
json_foreach(f, fields) {
|
|
char *key = f->string_;
|
|
|
|
if ((node = json_find_member(o, key)) != NULL) {
|
|
json_copy_element_to_object(newo, key, node);
|
|
}
|
|
}
|
|
json_delete(o);
|
|
o = newo;
|
|
|
|
}
|
|
json_append_element(locs, o);
|
|
++counter;
|
|
}
|
|
|
|
|
|
/* Add the (possibly) incremented counter back into `obj' */
|
|
json_append_member(obj, "count", json_mknumber(counter));
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* Read the file at `filename' (- is stdin) and store location
|
|
* objects at the JSON array `arr`. `obj' is a JSON object which
|
|
* contains `arr'.
|
|
* If limit is zero, we're going forward, else backwards.
|
|
* Fields, if not NULL, is a JSON array of desired element names.
|
|
*
|
|
* If username & device are not NULL, populate the JSON locations
|
|
* with them for multilister().
|
|
*/
|
|
|
|
void locations(char *filename, JsonNode *obj, JsonNode *arr, time_t s_lo, time_t s_hi, output_type otype, int limit, JsonNode *fields, char *username, char *device)
|
|
{
|
|
struct jparam jarg;
|
|
|
|
if (obj == NULL || obj->tag != JSON_OBJECT)
|
|
return;
|
|
|
|
jarg.obj = obj;
|
|
jarg.locs = arr;
|
|
jarg.s_lo = s_lo;
|
|
jarg.s_hi = s_hi;
|
|
jarg.otype = otype;
|
|
jarg.limit = limit;
|
|
jarg.fields = fields;
|
|
jarg.username = username;
|
|
jarg.device = device;
|
|
|
|
if (limit == 0) {
|
|
cat(filename, candidate_line, &jarg);
|
|
} else {
|
|
tac(filename, limit, candidate_line, &jarg);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* We're being passed an array of location objects created in
|
|
* locations(). Produce a Geo JSON tree.
|
|
|
|
{
|
|
"type": "FeatureCollection",
|
|
"features": [
|
|
{
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "Point",
|
|
"coordinates": [-80.83775386582222,35.24980190252168]
|
|
},
|
|
"properties": {
|
|
"name": "DOUBLE OAKS CENTER",
|
|
"address": "1326 WOODWARD AV"
|
|
}
|
|
},
|
|
{
|
|
"type": "Feature",
|
|
"geometry": {
|
|
"type": "Point",
|
|
"coordinates": [-80.83827000459532,35.25674709224663]
|
|
},
|
|
"properties": {
|
|
"name": "DOUBLE OAKS NEIGHBORHOOD PARK",
|
|
"address": "2605 DOUBLE OAKS RD"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
*
|
|
*/
|
|
|
|
static void append_to_feature_array(JsonNode *features, double lat, double lon, char *tid, char *addr, long tst, long vel, long acc, char *poi, char *isotst)
|
|
{
|
|
JsonNode *geom, *props, *f = json_mkobject();
|
|
|
|
json_append_member(f, "type", json_mkstring("Feature"));
|
|
|
|
geom = json_mkobject();
|
|
json_append_member(geom, "type", json_mkstring("Point"));
|
|
JsonNode *coords = json_mkarray();
|
|
json_append_element(coords, json_mknumber(lon)); /* first LON! */
|
|
json_append_element(coords, json_mknumber(lat));
|
|
json_append_member(geom, "coordinates", coords);
|
|
|
|
props = json_mkobject();
|
|
if (poi && *poi) {
|
|
json_append_member(props, "name", json_mkstring(poi));
|
|
} else {
|
|
json_append_member(props, "name", json_mkstring(tid));
|
|
json_append_member(props, "vel", json_mknumber(vel));
|
|
json_append_member(props, "tst", json_mknumber(tst));
|
|
json_append_member(props, "acc", json_mknumber(acc));
|
|
}
|
|
json_append_member(props, "address", json_mkstring(addr));
|
|
json_append_member(props, "isotst", json_mkstring(isotst));
|
|
|
|
json_append_member(f, "geometry", geom);
|
|
json_append_member(f, "properties", props);
|
|
|
|
json_append_element(features, f);
|
|
}
|
|
|
|
JsonNode *geo_json(JsonNode *location_array, bool poi_only)
|
|
{
|
|
JsonNode *one, *j;
|
|
JsonNode *feature_array, *fcollection;
|
|
|
|
if ((fcollection = json_mkobject()) == NULL)
|
|
return (NULL);
|
|
|
|
json_append_member(fcollection, "type", json_mkstring("FeatureCollection"));
|
|
|
|
feature_array = json_mkarray();
|
|
|
|
json_foreach(one, location_array) {
|
|
double lat = 0.0, lon = 0.0;
|
|
char *addr = "", *tid = "", *poi = "", *isotst = "";
|
|
long tst = 0, vel = 0, acc = 0;
|
|
|
|
if (poi_only) {
|
|
if ((j = json_find_member(one, "poi")) == NULL) {
|
|
continue;
|
|
}
|
|
poi = j->string_;
|
|
}
|
|
if ((j = json_find_member(one, "lat")) != NULL) {
|
|
lat = j->number_;
|
|
}
|
|
if ((j = json_find_member(one, "lon")) != NULL) {
|
|
lon = j->number_;
|
|
}
|
|
if ((j = json_find_member(one, "tid")) != NULL) {
|
|
tid = j->string_;
|
|
}
|
|
if ((j = json_find_member(one, "addr")) != NULL) {
|
|
addr = j->string_;
|
|
}
|
|
if ((j = json_find_member(one, "isotst")) != NULL) {
|
|
isotst = j->string_;
|
|
}
|
|
if ((j = json_find_member(one, "tst")) != NULL) {
|
|
tst = j->number_;
|
|
}
|
|
if ((j = json_find_member(one, "vel")) != NULL) {
|
|
vel = j->number_;
|
|
}
|
|
if ((j = json_find_member(one, "acc")) != NULL) {
|
|
acc = j->number_;
|
|
}
|
|
|
|
append_to_feature_array(feature_array, lat, lon, tid, addr, tst, vel, acc, poi, isotst);
|
|
}
|
|
|
|
json_append_member(fcollection, "features", feature_array);
|
|
|
|
return (fcollection);
|
|
}
|
|
|
|
/*
|
|
* Create JSON suitable to represent this LineString:
|
|
*
|
|
* {
|
|
* "geometry": {
|
|
* "coordinates": [
|
|
* [
|
|
* 17.814833,
|
|
* 59.604585
|
|
* ],
|
|
* [
|
|
* 17.814824,
|
|
* 59.60459
|
|
* ]
|
|
* ],
|
|
* "type": "LineString"
|
|
* },
|
|
* "type": "Feature",
|
|
* "properties": {}
|
|
* }
|
|
*
|
|
*/
|
|
JsonNode *geo_linestring(JsonNode *location_array)
|
|
{
|
|
JsonNode *top = json_mkobject(), *sorted;
|
|
|
|
json_append_member(top, "type", json_mkstring("Feature"));
|
|
json_append_member(top, "properties", json_mkobject());
|
|
|
|
JsonNode *c, *coords = json_mkarray();
|
|
|
|
sorted = listsort(json_first_child(location_array), 0, 0);
|
|
if (sorted && sorted->parent)
|
|
sorted = sorted->parent;
|
|
else
|
|
sorted = location_array;
|
|
|
|
json_foreach(c, sorted) {
|
|
JsonNode *lat, *lon;
|
|
|
|
if (((lat = json_find_member(c, "lat")) != NULL) &&
|
|
((lon = json_find_member(c, "lon")) != NULL)) {
|
|
|
|
JsonNode *latlon = json_mkarray();
|
|
|
|
json_append_element(latlon, json_mknumber(lon->number_));
|
|
json_append_element(latlon, json_mknumber(lat->number_));
|
|
|
|
json_append_element(coords, latlon);
|
|
}
|
|
}
|
|
|
|
JsonNode *geometry = json_mkobject();
|
|
|
|
json_append_member(geometry, "coordinates", coords);
|
|
json_append_member(geometry, "type", json_mkstring("LineString"));
|
|
json_append_member(top, "geometry", geometry);
|
|
|
|
return (top);
|
|
}
|
|
|
|
/*
|
|
* Turn our JSON location array into a GPX XML string.
|
|
*/
|
|
|
|
char *gpx_string(JsonNode *location_array)
|
|
{
|
|
JsonNode *one;
|
|
static UT_string *xml = NULL;
|
|
|
|
if (location_array->tag != JSON_ARRAY)
|
|
return (NULL);
|
|
|
|
utstring_renew(xml);
|
|
|
|
utstring_printf(xml, "%s", "<?xml version='1.0' encoding='UTF-8' standalone='no' ?>\n\
|
|
<gpx version='1.1' creator='OwnTracks-Recorder' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.topografix.com/GPX/1/1'>\n\
|
|
<trk>\n\
|
|
<trkseg>\n");
|
|
|
|
// <trkpt lat="xx.xxx" lon="yy.yyy"> <!-- Attribute des Trackpunkts --> </trkpt>
|
|
|
|
json_foreach(one, location_array) {
|
|
JsonNode *jlat, *jlon, *jisotst, *j;
|
|
|
|
if ((jlat = json_find_member(one, "lat")) &&
|
|
(jlon = json_find_member(one, "lon")) &&
|
|
(jisotst = json_find_member(one, "isotst"))) {
|
|
|
|
utstring_printf(xml, " <trkpt lat='%lf' lon='%lf'>\n", jlat->number_, jlon->number_);
|
|
utstring_printf(xml, "\t<time>%s</time>\n", jisotst->string_);
|
|
if ((j = json_find_member(one, "alt")) != NULL) {
|
|
utstring_printf(xml, "\t<ele>%.2f</ele>\n", j->number_);
|
|
}
|
|
utstring_printf(xml, "\t</trkpt>\n");
|
|
}
|
|
}
|
|
|
|
utstring_printf(xml, "%s", " </trkseg>\n</trk>\n</gpx>\n");
|
|
return (UB(xml));
|
|
}
|
|
|
|
#if WITH_KILL
|
|
|
|
/*
|
|
* Remove all data for a user's device. Return a JSON object with a status
|
|
* and an array of deleted files.
|
|
*/
|
|
|
|
static int kill_datastore_filter(const struct dirent *d)
|
|
{
|
|
return (*d->d_name == '.') ? 0 : 1;
|
|
}
|
|
|
|
JsonNode *kill_datastore(char *user, char *device)
|
|
{
|
|
static UT_string *path = NULL, *fname = NULL;
|
|
JsonNode *obj = json_mkobject(), *killed = json_mkarray();
|
|
char *bp;
|
|
struct dirent **namelist;
|
|
int i, n, rc;
|
|
glob_t results;
|
|
|
|
utstring_renew(path);
|
|
utstring_renew(fname);
|
|
|
|
if (!user || !*user || !device || !*device)
|
|
return (obj);
|
|
|
|
for (bp = user; bp && *bp; bp++) {
|
|
if (isupper(*bp))
|
|
*bp = tolower(*bp);
|
|
}
|
|
for (bp = device; bp && *bp; bp++) {
|
|
if (isupper(*bp))
|
|
*bp = tolower(*bp);
|
|
}
|
|
|
|
utstring_printf(path, "%s/rec/%s/%s", STORAGEDIR, user, device);
|
|
json_append_member(obj, "path", json_mkstring(UB(path)));
|
|
|
|
if ((n = scandir(UB(path), &namelist, kill_datastore_filter, NULL)) < 0) {
|
|
json_append_member(obj, "status", json_mkstring("ERROR"));
|
|
json_append_member(obj, "error", json_mkstring(strerror(errno)));
|
|
json_append_member(obj, "reason", json_mkstring("cannot scandir"));
|
|
return (obj);
|
|
}
|
|
|
|
for (i = 0; i < n; i++) {
|
|
char *p;
|
|
|
|
utstring_clear(fname);
|
|
utstring_printf(fname, "%s/%s", UB(path), namelist[i]->d_name);
|
|
|
|
p = UB(fname);
|
|
if (remove(p) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", p);
|
|
json_append_element(killed, json_mkstring(namelist[i]->d_name));
|
|
} else {
|
|
perror(p);
|
|
}
|
|
|
|
free(namelist[i]);
|
|
}
|
|
free(namelist);
|
|
|
|
json_append_member(obj, "status", json_mkstring("OK"));
|
|
if (rmdir(UB(path)) != 0) {
|
|
json_append_member(obj, "status", json_mkstring("ERROR"));
|
|
json_append_member(obj, "error", json_mkstring( strerror(errno)));
|
|
} else {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
|
|
/* Attempt to remove containing directory */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/rec/%s", STORAGEDIR, user);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
}
|
|
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/last/%s/%s/%s-%s.json", STORAGEDIR, user, device, user, device);
|
|
if (remove(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
json_append_member(obj, "last", json_mkstring(UB(path)));
|
|
}
|
|
|
|
/* Attempt to remove containing directory */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/last/%s/%s", STORAGEDIR, user, device);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
/* Attempt to remove it's parent directory */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/last/%s", STORAGEDIR, user);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
/* Attempt to remove CARD ... */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/cards/%s/%s.json", STORAGEDIR, user, user);
|
|
if (remove(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
json_append_member(obj, "card", json_mkstring(UB(path)));
|
|
}
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/cards/%s/%s/%s-%s.json", STORAGEDIR, user, device, user, device);
|
|
if (remove(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
json_append_member(obj, "card", json_mkstring(UB(path)));
|
|
}
|
|
|
|
/* ... and it's parent directories */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/cards/%s/%s", STORAGEDIR, user, device);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/cards/%s", STORAGEDIR, user);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
/* Attempt to remove PHOTO ... */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/photos/%s.png", STORAGEDIR, user);
|
|
if (remove(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
json_append_member(obj, "photo", json_mkstring(UB(path)));
|
|
}
|
|
|
|
/* ... and parent directory */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/photos/%s", STORAGEDIR, user);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
/* Remove config (.otrc) */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/config/%s-%s.otrc", STORAGEDIR, user, device);
|
|
if (remove(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
json_append_member(obj, "otrc", json_mkstring(UB(path)));
|
|
}
|
|
|
|
|
|
/* Remove waypoint files and containing directories */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/waypoints/%s/%s/*.json", STORAGEDIR, user, device);
|
|
rc = glob(UB(path), 0, 0, &results);
|
|
if (rc == 0) {
|
|
JsonNode *list = json_mkarray();
|
|
|
|
for (n = 0; n < results.gl_pathc; n++) {
|
|
if (remove(results.gl_pathv[n]) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", results.gl_pathv[n]);
|
|
json_append_element(list, json_mkstring(results.gl_pathv[n]));
|
|
}
|
|
}
|
|
json_append_member(obj, "waypoint", list);
|
|
}
|
|
globfree(&results);
|
|
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/waypoints/%s/%s/%s-%s.otrw", STORAGEDIR, user, device, user, device);
|
|
if (remove(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
json_append_member(obj, "otrw", json_mkstring(UB(path)));
|
|
}
|
|
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/waypoints/%s/%s", STORAGEDIR, user, device);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/waypoints/%s", STORAGEDIR, user);
|
|
if (rmdir(UB(path)) == 0) {
|
|
olog(LOG_NOTICE, "removed %s", UB(path));
|
|
}
|
|
|
|
json_append_member(obj, "killed", killed);
|
|
return (obj);
|
|
}
|
|
#endif /* WITH_KILL */
|
|
|
|
/*
|
|
* Print the value in a single JSON node as XML by invoking func(). If string,
|
|
* easy. If number account for what we call 'integer' types which shouldn't be
|
|
* printed as floats.
|
|
*/
|
|
|
|
static void emit_one(JsonNode *j, JsonNode *inttypes, void (func)(char *line, void *param), void *param)
|
|
{
|
|
static UT_string *line = NULL;
|
|
|
|
if (!strcmp(j->key, "_type"))
|
|
return;
|
|
|
|
utstring_renew(line);
|
|
utstring_printf(line, " <%s>", j->key);
|
|
/* Check if the value should be an "integer" (ie not float) */
|
|
if (j->tag == JSON_NUMBER) {
|
|
if (json_find_member(inttypes, j->key)) {
|
|
utstring_printf(line, "%.lf", j->number_);
|
|
} else {
|
|
utstring_printf(line, "%lf", j->number_);
|
|
}
|
|
} else if (j->tag == JSON_STRING) {
|
|
char *bp;
|
|
|
|
for (bp = j->string_; bp && *bp; bp++) {
|
|
switch (*bp) {
|
|
case '&': utstring_printf(line, "&"); break;
|
|
case '<': utstring_printf(line, "<"); break;
|
|
case '>': utstring_printf(line, ">"); break;
|
|
case '"': utstring_printf(line, """); break;
|
|
case '\'': utstring_printf(line, "'"); break;
|
|
default: utstring_printf(line, "%c", *bp); break;
|
|
}
|
|
}
|
|
} else if (j->tag == JSON_BOOL) {
|
|
utstring_printf(line, "%s", (j->bool_) ? "true" : "false");
|
|
} else if (j->tag == JSON_NULL) {
|
|
utstring_printf(line, "null");
|
|
}
|
|
utstring_printf(line, "</%s>", j->key);
|
|
func(UB(line), param);
|
|
}
|
|
|
|
void xml_output(JsonNode *array, output_type otype, JsonNode *fields, void (*func)(char *s, void *param), void *param)
|
|
{
|
|
JsonNode *node, *inttypes;
|
|
JsonNode *one, *j;
|
|
|
|
/* Prime the inttypes object with types we consider "integer" */
|
|
inttypes = json_mkobject();
|
|
json_append_member(inttypes, "batt", json_mkbool(1));
|
|
json_append_member(inttypes, "vel", json_mkbool(1));
|
|
json_append_member(inttypes, "cog", json_mkbool(1));
|
|
json_append_member(inttypes, "tst", json_mkbool(1));
|
|
json_append_member(inttypes, "alt", json_mkbool(1));
|
|
json_append_member(inttypes, "dist", json_mkbool(1));
|
|
json_append_member(inttypes, "trip", json_mkbool(1));
|
|
|
|
func("<?xml version='1.0' encoding='UTF-8'?>\n\
|
|
<?xml-stylesheet type='text/xsl' href='owntracks.xsl'?>", param);
|
|
func("<owntracks>", param);
|
|
|
|
json_foreach(one, array) {
|
|
func(" <point>", param);
|
|
if (fields) {
|
|
json_foreach(node, fields) {
|
|
if ((j = json_find_member(one, node->string_)) != NULL) {
|
|
emit_one(j, inttypes, func, param);
|
|
} else {
|
|
/* empty element */
|
|
char label[128];
|
|
|
|
snprintf(label, sizeof(label), " <%s />", node->string_);
|
|
func(label, param);
|
|
}
|
|
}
|
|
} else {
|
|
json_foreach(j, one) {
|
|
emit_one(j, inttypes, func, param);
|
|
}
|
|
}
|
|
func(" </point>\n", param);
|
|
}
|
|
func("</owntracks>", param);
|
|
|
|
json_delete(inttypes);
|
|
}
|
|
|
|
#define STRINGCOLUMN(x) (!strcmp(x, "addr") || !strcmp(x, "locality"))
|
|
|
|
/*
|
|
* Print the value in a single JSON node. If string, easy. If number account for
|
|
* what we call 'integer' types which shouldn't be printed as floats.
|
|
*/
|
|
|
|
static void print_one(UT_string *line, JsonNode *j, JsonNode *inttypes, void (func)(char *line, void *param), void *param)
|
|
{
|
|
/* Check if the value should be an "integer" (ie not float) */
|
|
if (j->tag == JSON_NUMBER) {
|
|
if (json_find_member(inttypes, j->key)) {
|
|
utstring_printf(line, "%.lf", j->number_);
|
|
} else {
|
|
utstring_printf(line, "%lf", j->number_);
|
|
}
|
|
} else if (j->tag == JSON_STRING) {
|
|
char *quote = "";
|
|
|
|
if (STRINGCOLUMN(j->key)) {
|
|
quote = "\"";
|
|
}
|
|
utstring_printf(line, "%s%s%s", quote, j->string_, quote);
|
|
} else if (j->tag == JSON_BOOL) {
|
|
utstring_printf(line, "%s", (j->bool_) ? "true" : "false");
|
|
} else if (j->tag == JSON_NULL) {
|
|
utstring_printf(line, "null");
|
|
}
|
|
}
|
|
|
|
static void csv_title(UT_string *line, JsonNode *node, char *column)
|
|
{
|
|
char *quote = "";
|
|
|
|
if (STRINGCOLUMN(column)) {
|
|
quote = "\"";
|
|
}
|
|
utstring_printf(line, "%s%s%s%c", quote, column, quote, (node->next) ? ',' : '\n');
|
|
}
|
|
|
|
/*
|
|
* Output location data as CSV. If `fields' is not NULL, it's a JSON
|
|
* array of JSON elment names which should be printed instead of the
|
|
* default ALL.
|
|
*/
|
|
|
|
void csv_output(JsonNode *array, output_type otype, JsonNode *fields, void (*func)(char *s, void *param), void *param)
|
|
{
|
|
JsonNode *node, *inttypes;
|
|
JsonNode *one, *j;
|
|
short virgin = 1;
|
|
static UT_string *line = NULL;
|
|
|
|
utstring_renew(line);
|
|
|
|
/* Prime the inttypes object with types we consider "integer" */
|
|
inttypes = json_mkobject();
|
|
json_append_member(inttypes, "batt", json_mkbool(1));
|
|
json_append_member(inttypes, "vel", json_mkbool(1));
|
|
json_append_member(inttypes, "cog", json_mkbool(1));
|
|
json_append_member(inttypes, "tst", json_mkbool(1));
|
|
json_append_member(inttypes, "alt", json_mkbool(1));
|
|
json_append_member(inttypes, "dist", json_mkbool(1));
|
|
json_append_member(inttypes, "trip", json_mkbool(1));
|
|
|
|
json_foreach(one, array) {
|
|
/* Headings */
|
|
if (virgin) {
|
|
|
|
virgin = !virgin;
|
|
|
|
if (fields) {
|
|
json_foreach(node, fields) {
|
|
csv_title(line, node, node->string_);
|
|
}
|
|
} else {
|
|
json_foreach(node, one) {
|
|
if (node->key)
|
|
csv_title(line, node, node->key);
|
|
}
|
|
}
|
|
func(UB(line), param);
|
|
utstring_renew(line);
|
|
}
|
|
|
|
/* Now the values */
|
|
if (fields) {
|
|
json_foreach(node, fields) {
|
|
if ((j = json_find_member(one, node->string_)) != NULL) {
|
|
print_one(line, j, inttypes, func, param);
|
|
utstring_printf(line, "%c", node->next ? ',' : '\n');
|
|
} else {
|
|
/* specified field not in JSON for this row */
|
|
utstring_printf(line, "%c", node->next ? ',' : '\n');
|
|
}
|
|
}
|
|
func(UB(line), param);
|
|
utstring_renew(line);
|
|
} else {
|
|
json_foreach(j, one) {
|
|
print_one(line, j, inttypes, func, param);
|
|
utstring_printf(line, "%c", j->next ? ',' : '\n');
|
|
}
|
|
func(UB(line), param);
|
|
utstring_renew(line);
|
|
}
|
|
}
|
|
json_delete(inttypes);
|
|
}
|
|
|
|
char *storage_userphoto(char *username)
|
|
{
|
|
static char path[LARGEBUF];
|
|
|
|
if (!username || !*username)
|
|
return (NULL);
|
|
|
|
snprintf(path, sizeof(path), "%s/photos/%s/%s.png", STORAGEDIR, username, username);
|
|
|
|
return (path);
|
|
}
|
|
|
|
/*
|
|
* array is an array of JSON objects. Get the JSON at http.json
|
|
* and append that object to `array`.
|
|
*/
|
|
|
|
void extra_http_json(JsonNode *array, char *user, char *device)
|
|
{
|
|
char path[LARGEBUF], *js_string;
|
|
JsonNode *node;
|
|
|
|
if (!array || !user || !*user || !device || !*device)
|
|
return;
|
|
if (array->tag != JSON_ARRAY)
|
|
return;
|
|
|
|
/* Extra data */
|
|
snprintf(path, LARGEBUF, "%s/last/%s/%s/http.json",
|
|
STORAGEDIR, user, device);
|
|
|
|
if ((js_string = slurp_file(path, TRUE)) == NULL) {
|
|
return;
|
|
}
|
|
|
|
if ((node = json_decode(js_string)) == NULL) {
|
|
olog(LOG_ERR, "extra_http_json: can't decode JSON from %s", path);
|
|
free(js_string);
|
|
return;
|
|
}
|
|
|
|
if (node->tag == JSON_OBJECT) {
|
|
json_append_element(array, node);
|
|
} else if (node->tag == JSON_ARRAY) {
|
|
JsonNode *f;
|
|
|
|
json_foreach(f, node) {
|
|
JsonNode *o;
|
|
|
|
if (f->tag != JSON_OBJECT)
|
|
continue;
|
|
|
|
o = json_mkobject();
|
|
json_copy_to_object(o, f, TRUE);
|
|
json_append_element(array, o);
|
|
}
|
|
json_delete(node);
|
|
}
|
|
free(js_string);
|
|
}
|
|
|
|
/*
|
|
* Process an array of waypoints as read from an .otrw file. If
|
|
* rad is positive and lat/lon exist, store in LMDB database for
|
|
* this user.
|
|
*/
|
|
|
|
static bool load_otrw_waypoints(struct udata *ud, JsonNode *wplist, char *user, char *device)
|
|
{
|
|
JsonNode *n;
|
|
static UT_string *key;
|
|
|
|
json_foreach(n, wplist) {
|
|
JsonNode *rad, *lat, *lon, *desc, *tst, *type;
|
|
|
|
if ((type = json_find_member(n, "_type")) == NULL)
|
|
return (false);
|
|
|
|
if (strcmp(type->string_, "waypoint") != 0)
|
|
return (false);
|
|
|
|
json_delete(type);
|
|
|
|
if ((rad = json_find_member(n, "rad")) == NULL)
|
|
continue;
|
|
if (rad->number_ <= 0)
|
|
continue;
|
|
|
|
lat = json_find_member(n, "lat");
|
|
lon = json_find_member(n, "lon");
|
|
tst = json_find_member(n, "tst");
|
|
desc = json_find_member(n, "desc");
|
|
|
|
/* It turns out tst was a key, but it breaks on iOS when dumped
|
|
* waypoints are re-imported. we'll use the geohash of lat/lon here */
|
|
|
|
json_delete(tst);
|
|
|
|
utstring_renew(key);
|
|
utstring_printf(key, "%s-%s-%s", user, device,
|
|
geohash_encode(lat->number_, lon->number_, 10));
|
|
|
|
olog(LOG_DEBUG, "--> %s: %s\t(%lf, %lf) (%ld)", UB(key), desc->string_, lat->number_, lon->number_, (long)rad->number_);
|
|
|
|
/*
|
|
* Just before initially storing in LMDB, we need to determine whether
|
|
* device is currently within or without a waypoint...
|
|
*/
|
|
|
|
/* Note: we don't need buf -- just checking if key exists */
|
|
// len = gcache_get(ud->wpdb, UB(key), buf, sizeof(buf));
|
|
// if (len == -1) {
|
|
// }
|
|
|
|
/* Clobber existing record b/c desc might have changed (#171) */
|
|
gcache_json_put(ud->wpdb, UB(key), n);
|
|
}
|
|
|
|
return (true);
|
|
}
|
|
|
|
void load_otrw_from_string(struct udata *ud, char *username, char *device, char *js_string)
|
|
{
|
|
JsonNode *node, *wplist;
|
|
|
|
if ((node = json_decode(js_string)) == NULL) {
|
|
olog(LOG_ERR, "load_otrw_from_string: can't decode JSON from string for %s-%s\n", username, device);
|
|
return;
|
|
}
|
|
|
|
if ((wplist = json_find_member(node, "waypoints"))) {
|
|
load_otrw_waypoints(ud, wplist, username, device);
|
|
}
|
|
|
|
json_delete(node);
|
|
}
|
|
|
|
static int load_otrw_file(struct udata *ud, char *filename)
|
|
{
|
|
char *js_string, *bp, *parts[20], *user, *device;
|
|
JsonNode *node, *wplist;
|
|
|
|
if ((js_string = slurp_file(filename, TRUE)) == NULL) {
|
|
return (false);
|
|
}
|
|
|
|
if ((node = json_decode(js_string)) == NULL) {
|
|
olog(LOG_ERR, "load_otrw_file: can't decode JSON from file s\n", filename);
|
|
free(js_string);
|
|
return (false);
|
|
}
|
|
|
|
/*
|
|
* Find username / device name from filename component of
|
|
* path (i.e. "/user-device.otrw").
|
|
*/
|
|
|
|
if ((bp = strrchr(filename, '/')) == NULL)
|
|
return (false);
|
|
if (splitter(bp+1, "-.", parts) != 3)
|
|
return (false);
|
|
|
|
user = parts[0];
|
|
device = parts[1];
|
|
|
|
if ((wplist = json_find_member(node, "waypoints"))) {
|
|
load_otrw_waypoints(ud, wplist, user, device);
|
|
}
|
|
|
|
splitterfree(parts);
|
|
free(js_string);
|
|
json_delete(node);
|
|
|
|
return (true);
|
|
}
|
|
|
|
bool load_fences(struct udata *ud)
|
|
{
|
|
static UT_string *path = NULL;
|
|
int n, rc;
|
|
glob_t results;
|
|
|
|
/* Get list of waypoint files */
|
|
utstring_renew(path);
|
|
utstring_printf(path, "%s/waypoints/*/*/*.otrw", STORAGEDIR);
|
|
rc = glob(UB(path), 0, 0, &results);
|
|
if (rc == 0) {
|
|
|
|
for (n = 0; n < results.gl_pathc; n++) {
|
|
char *f = results.gl_pathv[n];
|
|
|
|
load_otrw_file(ud, f);
|
|
}
|
|
}
|
|
globfree(&results);
|
|
|
|
return (true);
|
|
}
|