Files
recorder/storage.c
Jeremias Stotter f856975f05 Look up the timezone when an entry is recorded and store it
With big rec files, lookup might be incredibly slow because the timezone will be looked up for every entry.
Like this, timezone is recorded once when an entry is created and doesn't have to be looked up later
2026-01-10 22:25:17 +01:00

1752 lines
44 KiB
C

/*
* OwnTracks Recorder
* Copyright (C) 2015-2025 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 LARGEBUF (BUFSIZ * 2)
static struct gcache *gc = NULL;
#ifdef WITH_TZ
# include "zonedetect.h"
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) {
// Hide error; the directory will be created automatically later
// 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);
}
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);
}
#ifdef WITH_TZ
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);
}
}
}
#endif
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;
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_)));
#ifdef WITH_TZ
JsonNode *jlat, *jlon;
if ((jlat = json_find_member(last, "lat")) &&
(jlon = json_find_member(last, "lon"))) {
tz_info(last, jlat->number_, jlon->number_, jtst->number_);
}
#endif
}
}
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);
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 *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 ((o = json_decode(bp)) == NULL) {
return (NULL);
}
if ((j = json_find_member(o, "_type")) == NULL) {
json_delete(o);
return (NULL);
}
if (j->tag != JSON_STRING || strcmp(j->string_, "location") != 0) {
json_delete(o);
return (NULL);
}
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.
*/
#ifdef WITH_TZ
if ((j = json_find_member(o, "tzname")) != NULL) {
tzname = j->string_;
json_append_member(o, "isolocal", json_mkstring(isolocal(tst, tzname)));
} else {
/* if tzname is not in the object, i.e. it wasn't originally
* obtained during geo lookup and cached, then look it up
* in the binary zonedetect database now. Note that this will
* add quite a substantial amount of time to producing API
* results. We might need to make this run-time configurable.
*/
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, long alt, 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, "alt", json_mknumber(alt));
}
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, alt = 0;
if (poi_only) {
if ((j = json_find_member(one, "poi")) == NULL) {
continue;
}
if (j->tag != JSON_STRING)
continue;
poi = j->string_;
}
if ((j = json_find_member(one, "lat")) != NULL) {
if (j->tag != JSON_NUMBER)
continue;
lat = j->number_;
}
if ((j = json_find_member(one, "lon")) != NULL) {
if (j->tag != JSON_NUMBER)
continue;
lon = j->number_;
}
if ((j = json_find_member(one, "tid")) != NULL) {
if (j->tag != JSON_STRING)
continue;
tid = j->string_;
}
if ((j = json_find_member(one, "addr")) != NULL) {
if (j->tag != JSON_STRING)
continue;
addr = j->string_;
}
if ((j = json_find_member(one, "isotst")) != NULL) {
if (j->tag != JSON_STRING)
continue;
isotst = j->string_;
}
if ((j = json_find_member(one, "tst")) != NULL) {
if (j->tag != JSON_NUMBER)
continue;
tst = j->number_;
}
if ((j = json_find_member(one, "vel")) != NULL) {
if (j->tag != JSON_NUMBER)
continue;
vel = j->number_;
}
if ((j = json_find_member(one, "acc")) != NULL) {
if (j->tag != JSON_NUMBER)
continue;
acc = j->number_;
}
if ((j = json_find_member(one, "alt")) != NULL) {
if (j->tag != JSON_NUMBER)
continue;
alt = j->number_;
}
append_to_feature_array(feature_array, lat, lon, tid, addr, tst, vel, acc, alt, 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 {
olog(LOG_NOTICE, "kill_datastore: %s: %m", 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, "&amp;"); break;
case '<': utstring_printf(line, "&lt;"); break;
case '>': utstring_printf(line, "&gt;"); break;
case '"': utstring_printf(line, "&quot;"); break;
case '\'': utstring_printf(line, "&apos;"); 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);
}