added file backend

git-svn-id: https://zeitsenke.de/svn/SyncEvolution/trunk@701 15ad00c4-1369-45f4-8270-35d70d36bdcd
This commit is contained in:
Patrick Ohly 2008-08-03 19:51:53 +00:00
parent cc4cdb6c56
commit 76491facab
5 changed files with 665 additions and 0 deletions

View File

@ -0,0 +1,325 @@
/*
* Copyright (C) 2007 Patrick Ohly
* Copyright (C) 2007 Funambol
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#ifdef ENABLE_FILE
#include "FileSyncSource.h"
// SyncEvolution includes a copy of Boost header files.
// They are safe to use without creating additional
// build dependencies. boost::filesystem requires a
// library and therefore is avoided here. Some
// utility functions from SyncEvolution are used
// instead, plus standard C/Posix functions.
#include <boost/algorithm/string/case_conv.hpp>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <SyncEvolutionUtil.h>
#include <sstream>
#include <fstream>
FileSyncSource::FileSyncSource(const EvolutionSyncSourceParams &params,
const string &dataformat) :
TrackingSyncSource(params),
m_entryCounter(0)
{
if (dataformat.empty()) {
throwError("a data format must be specified");
}
size_t sep = dataformat.find(':');
if (sep == dataformat.npos) {
throwError(string("data format not specified as <mime type>:<mime version>: " + dataformat));
}
m_mimeType.assign(dataformat, 0, sep);
m_mimeVersion = dataformat.substr(sep + 1);
m_supportedTypes = dataformat;
}
string FileSyncSource::fileSuffix() const
{
// database dumps created by SyncEvolution use this file suffix
return
(m_mimeType == "text/vcard" || m_mimeType == "text/x-vcard") ? "vcf" :
(m_mimeType == "text/calendar" || m_mimeType == "text/x-calendar") ? "ics" :
(m_mimeType == "text/plain") ? "txt" :
"dat";
}
const char *FileSyncSource::getMimeType() const
{
return m_mimeType.c_str();
}
const char *FileSyncSource::getMimeVersion() const
{
return m_mimeVersion.c_str();
}
const char *FileSyncSource::getSupportedTypes() const
{
// comma separated list, like "text/vcard:3.0,text/x-vcard:2.1"
return m_supportedTypes.c_str();
}
void FileSyncSource::open()
{
const string &database = getDatabaseID();
const string prefix("file://");
string basedir;
bool createDir = false;
// file:// is optional. It indicates that the
// directory is to be created.
if (boost::starts_with(database, prefix)) {
basedir = database.substr(prefix.size());
createDir = true;
} else {
basedir = database;
}
// check and, if allowed and necessary, create it
if (!isDir(basedir)) {
if (errno == ENOENT && createDir) {
mkdir_p(basedir.c_str());
} else {
throwError(basedir + ": " + strerror(errno));
}
}
// success!
m_basedir = basedir;
}
void FileSyncSource::close()
{
m_basedir.clear();
}
FileSyncSource::Databases FileSyncSource::getDatabases()
{
Databases result;
result.push_back(Database("select database via directory path",
"[file://]<path>"));
return result;
}
void FileSyncSource::listAllItems(RevisionMap_t &revisions)
{
ReadDir dirContent(m_basedir);
BOOST_FOREACH(const string &entry, dirContent) {
string filename = createFilename(entry);
string revision = getATimeString(filename);
long entrynum = atoll(entry.c_str());
if (entrynum >= m_entryCounter) {
m_entryCounter = entrynum + 1;
}
revisions[entry] = revision;
}
}
SyncItem *FileSyncSource::createItem(const string &uid)
{
string filename = createFilename(uid);
ifstream in;
in.open(filename.c_str());
ostringstream out;
char buf[8192];
do {
in.read(buf, sizeof(buf));
out.write(buf, in.gcount());
} while(in);
if (!in.good() && !in.eof()) {
throwError(filename + ": reading failed");
}
string content = out.str();
auto_ptr<SyncItem> item(new SyncItem(uid.c_str()));
item->setData(content.c_str(), content.size());
item->setDataType(getMimeType());
// probably not even used by Funambol client library...
item->setModificationTime(0);
return item.release();
}
TrackingSyncSource::InsertItemResult FileSyncSource::insertItem(const string &uid, const SyncItem &item)
{
string newuid = uid;
string creationTime;
string filename;
// Inserting a new and updating an existing item often uses
// very similar code. In this case only the code for determining
// the filename differs.
//
// In other sync sources the database might also have limitations
// for the content of different items, for example, only one
// VCALENDAR:EVENT with a certain UID. If the server does not
// recognize that and sends a new item which collides with an
// existing one, then the existing one should be updated.
if (uid.size()) {
// valid local ID: update that file
filename = createFilename(uid);
} else {
// no local ID: create new file
while (true) {
ostringstream buff;
buff << m_entryCounter;
filename = createFilename(buff.str());
// only create and truncate if file does not
// exist yet, otherwise retry with next counter
struct stat dummy;
if (stat(filename.c_str(), &dummy)) {
if (errno == ENOENT) {
newuid = buff.str();
break;
} else {
throwError(filename + ": " + strerror(errno));
}
}
m_entryCounter++;
}
}
ofstream out;
out.open(filename.c_str());
out.write((const char *)item.getData(), item.getDataSize());
out.close();
if (!out.good()) {
throwError(filename + ": writing failed");
}
return InsertItemResult(newuid,
getATimeString(filename),
false /* true if adding item was turned into update */);
}
void FileSyncSource::deleteItem(const string &uid)
{
string filename = createFilename(uid);
if (unlink(filename.c_str())) {
throwError(filename + ": " + strerror(errno));
}
}
void FileSyncSource::flush()
{
// Our change tracking is time based.
// Don't let caller proceed without waiting for
// one second to prevent being called again before
// the modification time stamp is larger than it
// is now.
sleep(1);
}
void FileSyncSource::logItem(const string &uid, const string &info, bool debug)
{
if (LOG.getLevel() >= (debug ? LOG_LEVEL_DEBUG : LOG_LEVEL_INFO)) {
// If there was a good way to extract a short string identifying
// the item with uid, we would use it here and log it like this:
// (LOG.*(debug ? &Log::debug : &Log::info))("%s: %s %s",
// getName() /* out sync source name */,
// itemName,
// info.c_str());
//
// Alternatively we could just log the uid. EvolutionSyncSource::logItem()
// is an utility function which extracts a short string from certain
// well-known types (FN for vCard, SUMMARY for vCalendar, first line for
// text, ...). We use it here although it requires reading the full item
// first. Don't fail while reading, we'll trigger a real error later on
// if necessary.
string filename = createFilename(uid);
ifstream in;
in.open(filename.c_str());
ostringstream out;
char buf[8192];
do {
in.read(buf, sizeof(buf));
out.write(buf, in.gcount());
} while(in);
logItemUtil(out.str(),
m_mimeType,
m_mimeVersion,
uid,
info,
debug);
}
}
void FileSyncSource::logItem(const SyncItem &item, const string &info, bool debug)
{
if (LOG.getLevel() >= (debug ? LOG_LEVEL_DEBUG : LOG_LEVEL_INFO)) {
if (!item.getData()) {
// operation on item without data, fall back to logging via uid
logItem(string(item.getKey()), info, debug);
} else {
string data = (const char *)item.getData();
logItemUtil(data,
m_mimeType,
m_mimeVersion,
item.getKey(),
info,
debug);
}
}
}
string FileSyncSource::getATimeString(const string &filename)
{
struct stat buf;
if (stat(filename.c_str(), &buf)) {
throwError(filename + ": " + strerror(errno));
}
time_t mtime = buf.st_mtime;
ostringstream revision;
revision << mtime;
return revision.str();
}
string FileSyncSource::createFilename(const string &entry)
{
string filename = m_basedir + "/" + entry;
return filename;
}
#endif /* ENABLE_FILE */
#ifdef ENABLE_MODULES
# include "FileSyncSourceRegister.cpp"
#endif

View File

@ -0,0 +1,111 @@
/*
* Copyright (C) 2007 Patrick Ohly
* Copyright (C) 2007 Funambol
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef INCL_FILESYNCSOURCE
#define INCL_FILESYNCSOURCE
#include "TrackingSyncSource.h"
#ifdef ENABLE_FILE
#include <memory>
#include <boost/noncopyable.hpp>
/**
* Stores each SyncML item as a separate file in a directory. The
* directory has to be specified via the database name, using
* [file://]<path> as format. The file:// prefix is optional, but the
* directory is only created if it is used.
* EvolutionSyncSource::getDatabaseID() gives us the database name.
*
* Change tracking is done via the file systems modification time
* stamp: editing a file treats it as modified and then sends it to
* the server in the next sync. Removing and adding files also works.
*
* The local unique identifier for each item is its name in the
* directory. New files are created using a running count which
* initialized based on the initial content of the directory to
* "highest existing number + 1" and incremented to avoid collisions.
*
* Although this sync source itself does not care about the content of
* each item/file, the server needs to know what each item sent to it
* contains and what items the source is able to receive. Therefore
* the "type" property for this source must contain a data format
* specified, including a version for it. Here are some examples:
* - type=file:text/vcard:3.0
* - type=file:text/plain:1.0
*/
class FileSyncSource : public TrackingSyncSource, private boost::noncopyable
{
public:
FileSyncSource(const EvolutionSyncSourceParams &params,
const string &dataformat);
protected:
/* implementation of EvolutionSyncSource interface */
virtual void open();
virtual void close();
virtual Databases getDatabases();
virtual SyncItem *createItem(const string &uid);
virtual string fileSuffix() const;
virtual const char *getMimeType() const;
virtual const char *getMimeVersion() const;
virtual const char *getSupportedTypes() const;
virtual void logItem(const string &uid, const string &info, bool debug = false);
virtual void logItem(const SyncItem &item, const string &info, bool debug = false);
/* implementation of TrackingSyncSource interface */
virtual void listAllItems(RevisionMap_t &revisions);
virtual InsertItemResult insertItem(const string &uid, const SyncItem &item);
virtual void deleteItem(const string &uid);
virtual void flush();
private:
/**
* @name values obtained from the source's type property
*
* Other sync sources only support one hard-coded type and
* don't need such variables.
*/
/**@{*/
string m_mimeType;
string m_mimeVersion;
string m_supportedTypes;
/**@}*/
/** directory selected via the database name in open(), reset in close() */
string m_basedir;
/** a counter which is used to name new files */
long m_entryCounter;
/**
* get access time for file, formatted as revision string
* @param filename absolute path or path relative to current directory
*/
string getATimeString(const string &filename);
/**
* create full filename from basedir and entry name
*/
string createFilename(const string &entry);
};
#endif // ENABLE_FILE
#endif // INCL_FILESYNCSOURCE

View File

@ -0,0 +1,153 @@
/*
* Copyright (C) 2008 Patrick Ohly
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "FileSyncSource.h"
// The anonymous namespace ensures that we don't get
// name clashes: although the classes and objects are
// only defined in this file, the methods generated
// for local classes will clash with other methods
// of other classes with the same name if no namespace
// is used.
namespace {
#if 0
}
#endif
static EvolutionSyncSource *createSource(const EvolutionSyncSourceParams &params)
{
pair <string, string> sourceType = EvolutionSyncSource::getSourceType(params.m_nodes);
// The string returned by getSourceType() is always the one
// registered as main Aliases() below.
bool isMe = sourceType.first == "Files in one directory";
#ifndef ENABLE_FILE
// tell SyncEvolution if the user wanted to use a disabled sync source,
// otherwise let it continue searching
return isMe ? RegisterSyncSource::InactiveSource : NULL;
#else
// Also recognize one of the standard types?
// Not in the FileSyncSource!
bool maybeMe = false /* sourceType.first == "addressbook" */;
if (isMe || maybeMe) {
// The FileSyncSource always needs the data format
// parameter in sourceType.second.
if (/* sourceType.second == "" || sourceType.second == "text/x-vcard" */
sourceType.second.size()) {
return new FileSyncSource(params, sourceType.second);
} else {
return NULL;
}
}
return NULL;
#endif
}
static RegisterSyncSource registerMe("Files in one directory",
#ifdef ENABLE_FILE
true,
#else
false,
#endif
createSource,
"Files in one directory = file\n"
" Stores items in one directory as one file per item.\n"
" The directory is selected via [file://]<path>; it\n"
" will only be created if the prefix is given, otherwise\n"
" it must exist already. Only items of the same type can\n"
" be synchronized and this type must be specified explicitly\n"
" with both mime type and version.\n"
" Examples:\n"
" file:text/plain:1.0\n"
" file:text/x-vcard:2.1\n"
" file:text/vcard:3.0\n"
" file:text/x-calendar:1.0\n"
" file:text/calendar:2.0\n",
Values() +
(Aliases("Files in one directory") + "file"));
#ifdef ENABLE_FILE
#ifdef ENABLE_UNIT_TESTS
class FileSyncSourceUnitTest : public CppUnit::TestFixture {
CPPUNIT_TEST_SUITE(FileSyncSourceUnitTest);
CPPUNIT_TEST(testInstantiate);
CPPUNIT_TEST_SUITE_END();
protected:
void testInstantiate() {
boost::shared_ptr<EvolutionSyncSource> source;
source.reset(EvolutionSyncSource::createTestingSource("file", "file:text/vcard:3.0", true));
source.reset(EvolutionSyncSource::createTestingSource("file", "file:text/plain:1.0", true));
source.reset(EvolutionSyncSource::createTestingSource("file", "Files in one directory:text/x-vcard:2.1", true));
}
};
SYNCEVOLUTION_TEST_SUITE_REGISTRATION(FileSyncSourceUnitTest);
#endif // ENABLE_UNIT_TESTS
#ifdef ENABLE_INTEGRATION_TESTS
static class FileSyncSourceVCard21Test : public RegisterSyncSourceTest {
public:
FileSyncSourceVCard21Test() : RegisterSyncSourceTest("file_vcard21", "vcard21") {}
virtual void updateConfig(ClientTestConfig &config) const
{
// set type as required by FileSyncSource
// and leave everything else at its default
config.type = "file:text/x-vcard:2.1";
}
} FileSyncSourceVCard21Test;
static class FileSyncSourceVCard30Test : public RegisterSyncSourceTest {
public:
FileSyncSourceVCard30Test() : RegisterSyncSourceTest("file_vcard30", "vcard30") {}
virtual void updateConfig(ClientTestConfig &config) const
{
config.type = "file:text/vcard:3.0";
}
} FileSyncSourceVCard30Test;
static class FileSyncSourceICal20Test : public RegisterSyncSourceTest {
public:
FileSyncSourceICal20Test() : RegisterSyncSourceTest("file_ical20", "ical20") {}
virtual void updateConfig(ClientTestConfig &config) const
{
config.type = "file:text/calendar:2.0";
}
} FileSyncSourceICal20Test;
static class FileSyncSourceITodo20Test : public RegisterSyncSourceTest {
public:
FileSyncSourceITodo20Test() : RegisterSyncSourceTest("file_itodo20", "itodo20") {}
virtual void updateConfig(ClientTestConfig &config) const
{
config.type = "file:text/calendar:2.0";
}
} FileSyncSourceITodo20Test;
#endif // ENABLE_INTEGRATION_TESTS
#endif // ENABLE_FILE
}

View File

@ -0,0 +1,39 @@
# Some of the core header files still have a dependency on Evolution,
# which is why its flags have to be listed. They are empty if Evolution
# access is disabled.
AM_CPPFLAGS = -I$(srcdir)/../../core @EPACKAGE_CFLAGS@ @EBOOK_CFLAGS@ @ECAL_CFLAGS@ @FILE_CFLAGS@ @FUNAMBOL_CFLAGS@
# Applies to sources in SyncEvolution repository, but not
# the Funambol C++ client library. Used to add -Wall -Werror
# only when compiling that source, but not for the client
# library.
SYNCEVOLUTION_CXXFLAGS = @SYNCEVOLUTION_CXXFLAGS@
EXTRA_DIST = configure-sub.in
SYNCSOURCES = syncfile.la
MOSTLYCLEANFILES = $(SYNCSOURCES)
if ENABLE_MODULES
pkglib_LTLIBRARIES = $(SYNCSOURCES)
else
noinst_LTLIBRARIES = $(SYNCSOURCES)
endif
MAINTAINERCLEANFILES = Makefile.in
SYNCFILE_SOURCES = \
FileSyncSource.h \
FileSyncSource.cpp
syncfile_la_SOURCES = $(SYNCFILE_SOURCES)
syncfile_la_LIBADD = @FILE_LIBS@
syncfile_la_LDFLAGS = -module -rpath '$(pkglibdir)'
syncfile_la_CXXFLAGS = $(SYNCEVOLUTION_CXXFLAGS)
# If you need special test cases for your sync source, then
# install them here. Here's how the sqlite backend does that:
#
#../../testcases/sqlite_vcard21.vcf: $(FUNAMBOL_SUBDIR)/test/test/testcases/vcard21.vcf
# mkdir -p ${@D}
# perl -e '$$_ = join("", <>); s/^(ADR|TEL|EMAIL|PHOTO).*?(?=^\S)//msg; s/;X-EVOLUTION-UI-SLOT=\d+//g; print;' $< >$@
#all: ../../testcases/sqlite_vcard21.vcf

View File

@ -0,0 +1,37 @@
dnl -*- mode: Autoconf; -*-
dnl Invoke autogen.sh to produce a configure script.
dnl Checks for required libraris can go here; none required for simple files.
dnl
dnl This is from the sqlite backend:
dnl PKG_CHECK_MODULES(SQLITE, sqlite3, SQLITEFOUND=yes, [SQLITEFOUND=no])
dnl AC_SUBST(SQLITE_CFLAGS)
dnl AC_SUBST(SQLITE_LIBS)
FILE_CFLAGS=
FILE_LIBS=
AC_SUBST(FILE_CFLAGS)
AC_SUBST(FILE_LIBS)
dnl If additional compile flags are necessary to include the header
dnl files of the backend, then add them here.
BACKEND_CPPFLAGS="$BACKEND_CPPFLAGS $FILE_CFLAGS"
dnl name of backend library (there could be more than one per directory),
dnl name of the directory,
dnl help string,
dnl --enable/disable chosen explicitly
dnl default, may depend on availability of prerequisites in more complex backends
AC_ARG_ENABLE_BACKEND(file,
file,
AS_HELP_STRING([--disable-file],
[disable file-based backend which stores items in separate files in a fixed directory (default on)]),
[enable_file="$enableval"],
[enable_file="yes"]
)
if test "$enable_file" = "yes"; then
dnl It's good to check the prerequisites here, in case --enable-file was used.
dnl test "x${SQLITEFOUND}" == "xyes" || AC_MSG_ERROR([--enable-sqlite requires pkg-config information for sqlite3, which was not found])
AC_DEFINE(ENABLE_FILE, 1, [file available])
fi