From ecec803388bf6ef6aac4c451a0ff08518295ef73 Mon Sep 17 00:00:00 2001 From: James Turner Date: Thu, 25 Oct 2012 16:45:59 +0100 Subject: [PATCH] SVN read-only client code using our HTTP engine. --- simgear/io/CMakeLists.txt | 21 ++ simgear/io/DAVMultiStatus.cxx | 385 ++++++++++++++++++++++ simgear/io/DAVMultiStatus.hxx | 141 ++++++++ simgear/io/SVNDirectory.cxx | 368 +++++++++++++++++++++ simgear/io/SVNDirectory.hxx | 112 +++++++ simgear/io/SVNReportParser.cxx | 586 +++++++++++++++++++++++++++++++++ simgear/io/SVNReportParser.hxx | 57 ++++ simgear/io/SVNRepository.cxx | 414 +++++++++++++++++++++++ simgear/io/SVNRepository.hxx | 76 +++++ simgear/io/http_svn.cxx | 49 +++ 10 files changed, 2209 insertions(+) create mode 100644 simgear/io/DAVMultiStatus.cxx create mode 100644 simgear/io/DAVMultiStatus.hxx create mode 100644 simgear/io/SVNDirectory.cxx create mode 100644 simgear/io/SVNDirectory.hxx create mode 100644 simgear/io/SVNReportParser.cxx create mode 100644 simgear/io/SVNReportParser.hxx create mode 100644 simgear/io/SVNRepository.cxx create mode 100644 simgear/io/SVNRepository.hxx create mode 100644 simgear/io/http_svn.cxx diff --git a/simgear/io/CMakeLists.txt b/simgear/io/CMakeLists.txt index 8170b226..963e0436 100644 --- a/simgear/io/CMakeLists.txt +++ b/simgear/io/CMakeLists.txt @@ -16,6 +16,10 @@ set(HEADERS sg_socket_udp.hxx HTTPClient.hxx HTTPRequest.hxx + DAVMultiStatus.hxx + SVNRepository.hxx + SVNDirectory.hxx + SVNReportParser.hxx ) set(SOURCES @@ -32,11 +36,28 @@ set(SOURCES sg_socket_udp.cxx HTTPClient.cxx HTTPRequest.cxx + DAVMultiStatus.cxx + SVNRepository.cxx + SVNDirectory.cxx + SVNReportParser.cxx ) simgear_component(io io "${SOURCES}" "${HEADERS}") if(ENABLE_TESTS) + +if (SIMGEAR_SHARED) + set(TEST_LIBS SimGearCore) +else() + set(TEST_LIBS SimGearCore + ${CMAKE_THREAD_LIBS_INIT} + ${WINSOCK_LIBRARY} + ${ZLIB_LIBRARY} + ${RT_LIBRARY}) +endif() + +add_executable(http_svn http_svn.cxx) +target_link_libraries(http_svn ${TEST_LIBS}) add_executable(test_sock socktest.cxx) target_link_libraries(test_sock ${TEST_LIBS}) diff --git a/simgear/io/DAVMultiStatus.cxx b/simgear/io/DAVMultiStatus.cxx new file mode 100644 index 00000000..54c23870 --- /dev/null +++ b/simgear/io/DAVMultiStatus.cxx @@ -0,0 +1,385 @@ +// DAVMultiStatus.cxx -- parser for WebDAV MultiStatus XML data +// +// Copyright (C) 2012 James Turner +// +// 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. + +#include "DAVMultiStatus.hxx" + +#include +#include +#include +#include +#include + +#include + +#include "simgear/debug/logstream.hxx" +#include "simgear/xml/xmlparse.h" +#include "simgear/misc/strutils.hxx" +#include "simgear/structure/exception.hxx" + +using std::cout; +using std::cerr; +using std::endl; +using std::string; + +using namespace simgear; + +#define DAV_NS "DAV::" +#define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/" + +const char* DAV_MULTISTATUS_TAG = DAV_NS "multistatus"; +const char* DAV_RESPONSE_TAG = DAV_NS "response"; +const char* DAV_PROPSTAT_TAG = DAV_NS "propstat"; +const char* DAV_PROP_TAG = DAV_NS "prop"; + +const char* DAV_HREF_TAG = DAV_NS "href"; +const char* DAV_RESOURCE_TYPE_TAG = DAV_NS "resourcetype"; +const char* DAV_CONTENT_TYPE_TAG = DAV_NS "getcontenttype"; +const char* DAV_CONTENT_LENGTH_TAG = DAV_NS "getcontentlength"; +const char* DAV_VERSIONNAME_TAG = DAV_NS "version-name"; +const char* DAV_COLLECTION_TAG = DAV_NS "collection"; +const char* DAV_VCC_TAG = DAV_NS "version-controlled-configuration"; + +const char* SUBVERSION_MD5_CHECKSUM_TAG = SUBVERSION_DAV_NS ":md5-checksum"; + +DAVResource::DAVResource(const string& href) : + _type(Unknown), + _url(href), + _container(NULL) +{ + assert(!href.empty()); +} + +void DAVResource::setVersionName(const std::string& aVersion) +{ + _versionName = aVersion; +} + +void DAVResource::setVersionControlledConfiguration(const std::string& vcc) +{ + _vcc = vcc; +} + +void DAVResource::setMD5(const std::string& md5Hex) +{ + _md5 = md5Hex; +} + +std::string DAVResource::name() const +{ + string::size_type index = _url.rfind('/'); + if (index != string::npos) { + return _url.substr(index + 1); + } + + throw sg_exception("bad DAV resource HREF:" + _url); +} + +//////////////////////////////////////////////////////////////////////////// + +DAVCollection::DAVCollection(const string& href) : + DAVResource(href) +{ + _type = DAVResource::Collection; +} + +DAVCollection::~DAVCollection() +{ + BOOST_FOREACH(DAVResource* c, _contents) { + delete c; + } +} + +void DAVCollection::addChild(DAVResource *res) +{ + assert(res); + if (res->container() == this) { + return; + } + + assert(res->container() == NULL); + assert(std::find(_contents.begin(), _contents.end(), res) == _contents.end()); + + if (!strutils::starts_with(res->url(), _url)) { + std::cerr << "us: " << _url << std::endl; + std::cerr << "child:" << res->url() << std::endl; + + } + + assert(strutils::starts_with(res->url(), _url)); + assert(childWithUrl(res->url()) == NULL); + + res->_container = this; + _contents.push_back(res); +} + +void DAVCollection::removeChild(DAVResource* res) +{ + assert(res); + assert(res->container() == this); + + res->_container = NULL; + DAVResourceList::iterator it = std::find(_contents.begin(), _contents.end(), res); + assert(it != _contents.end()); + _contents.erase(it); +} + +DAVCollection* +DAVCollection::createChildCollection(const std::string& name) +{ + DAVCollection* child = new DAVCollection(urlForChildWithName(name)); + addChild(child); + return child; +} + +DAVResourceList DAVCollection::contents() const +{ + return _contents; +} + +DAVResource* DAVCollection::childWithUrl(const string& url) const +{ + if (url.empty()) + return NULL; + + BOOST_FOREACH(DAVResource* c, _contents) { + if (c->url() == url) { + return c; + } + } + + return NULL; +} + +DAVResource* DAVCollection::childWithName(const string& name) const +{ + return childWithUrl(urlForChildWithName(name)); +} + +std::string DAVCollection::urlForChildWithName(const std::string& name) const +{ + return url() + "/" + name; +} + +/////////////////////////////////////////////////////////////////////////////// + +class DAVMultiStatus::DAVMultiStatusPrivate +{ +public: + DAVMultiStatusPrivate() : + parserInited(false) + { + rootResource = NULL; + } + + void startElement (const char * name) + { + if (tagStack.empty()) { + if (strcmp(name, DAV_MULTISTATUS_TAG)) { + SG_LOG(SG_IO, SG_WARN, "root element is not " << + DAV_MULTISTATUS_TAG << ", got:" << name); + } else { + + } + } else { + // not at the root element + if (tagStack.back() == DAV_MULTISTATUS_TAG) { + if (strcmp(name, DAV_RESPONSE_TAG)) { + SG_LOG(SG_IO, SG_WARN, "multistatus child is not response: saw:" + << name); + } + } + + if (tagStack.back() == DAV_RESOURCE_TYPE_TAG) { + if (!strcmp(name, DAV_COLLECTION_TAG)) { + currentElementType = DAVResource::Collection; + } else { + currentElementType = DAVResource::Unknown; + } + } + } + + tagStack.push_back(name); + if (!strcmp(name, DAV_RESPONSE_TAG)) { + currentElementType = DAVResource::Unknown; + currentElementUrl.clear(); + currentElementMD5.clear(); + currentVersionName.clear(); + currentVCC.clear(); + } + } + + void endElement (const char * name) + { + assert(tagStack.back() == name); + tagStack.pop_back(); + + if (!strcmp(name, DAV_RESPONSE_TAG)) { + // finish complete response + currentElementUrl = strutils::strip(currentElementUrl); + + DAVResource* res = NULL; + if (currentElementType == DAVResource::Collection) { + DAVCollection* col = new DAVCollection(currentElementUrl); + res = col; + } else { + res = new DAVResource(currentElementUrl); + } + + res->setVersionName(strutils::strip(currentVersionName)); + res->setVersionControlledConfiguration(currentVCC); + if (rootResource && + strutils::starts_with(currentElementUrl, rootResource->url())) + { + static_cast(rootResource)->addChild(res); + } + + if (!rootResource) { + rootResource = res; + } + } + } + + void data (const char * s, int length) + { + if (tagStack.back() == DAV_HREF_TAG) { + if (tagN(1) == DAV_RESPONSE_TAG) { + currentElementUrl += string(s, length); + } else if (tagN(1) == DAV_VCC_TAG) { + currentVCC += string(s, length); + } + } else if (tagStack.back() == SUBVERSION_MD5_CHECKSUM_TAG) { + currentElementMD5 = string(s, length); + } else if (tagStack.back() == DAV_VERSIONNAME_TAG) { + currentVersionName = string(s, length); + } else if (tagStack.back() == DAV_CONTENT_LENGTH_TAG) { + std::istringstream is(string(s, length)); + is >> currentElementLength; + } + } + + void pi (const char * target, const char * data) {} + + string tagN(const unsigned int n) const + { + int sz = tagStack.size(); + if (n >= sz) { + return string(); + } + + return tagStack[sz - (1 + n)]; + } + + bool parserInited; + XML_Parser xmlParser; + DAVResource* rootResource; + + // in-flight data + string_list tagStack; + DAVResource::Type currentElementType; + string currentElementUrl, + currentVersionName, + currentVCC; + int currentElementLength; + string currentElementMD5; +}; + + +//////////////////////////////////////////////////////////////////////// +// Static callback functions for Expat. +//////////////////////////////////////////////////////////////////////// + +#define VISITOR static_cast(userData) + +static void +start_element (void * userData, const char * name, const char ** atts) +{ + VISITOR->startElement(name); +} + +static void +end_element (void * userData, const char * name) +{ + VISITOR->endElement(name); +} + +static void +character_data (void * userData, const char * s, int len) +{ + VISITOR->data(s, len); +} + +static void +processing_instruction (void * userData, + const char * target, + const char * data) +{ + VISITOR->pi(target, data); +} + +#undef VISITOR + +/////////////////////////////////////////////////////////////////////////////// + +DAVMultiStatus::DAVMultiStatus() : +_d(new DAVMultiStatusPrivate) +{ + +} + +DAVMultiStatus::~DAVMultiStatus() +{ + +} + +void DAVMultiStatus::parseXML(const char* data, int size) +{ + if (!_d->parserInited) { + _d->xmlParser = XML_ParserCreateNS(0, ':'); + XML_SetUserData(_d->xmlParser, _d.get()); + XML_SetElementHandler(_d->xmlParser, start_element, end_element); + XML_SetCharacterDataHandler(_d->xmlParser, character_data); + XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction); + _d->parserInited = true; + } + + if (!XML_Parse(_d->xmlParser, data, size, false)) { + SG_LOG(SG_IO, SG_WARN, "DAV parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser)) + << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser) + << " column " << XML_GetCurrentColumnNumber(_d->xmlParser)); + + XML_ParserFree(_d->xmlParser); + _d->parserInited = false; + } +} + +void DAVMultiStatus::finishParse() +{ + if (_d->parserInited) { + XML_Parse(_d->xmlParser, NULL, 0, true); + XML_ParserFree(_d->xmlParser); + } + + _d->parserInited = false; +} + +DAVResource* DAVMultiStatus::resource() +{ + return _d->rootResource; +} + + diff --git a/simgear/io/DAVMultiStatus.hxx b/simgear/io/DAVMultiStatus.hxx new file mode 100644 index 00000000..862bf9d0 --- /dev/null +++ b/simgear/io/DAVMultiStatus.hxx @@ -0,0 +1,141 @@ +// DAVMultiStatus.hxx -- parser for WebDAV MultiStatus XML data +// +// Copyright (C) 2012 James Turner +// +// 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. + + +#ifndef SG_IO_DAVMULTISTATUS_HXX +#define SG_IO_DAVMULTISTATUS_HXX + +#include +#include +#include // for auto_ptr + +namespace simgear +{ + +class DAVCollection; + +class DAVResource +{ +public: + DAVResource(const std::string& url); + virtual ~DAVResource() { } + + typedef enum { + Unknown = 0, + Collection = 1 + } Type; + + const Type type() const + { return _type; } + + const std::string& url() const + { return _url; } + + std::string name() const; + + /** + * SVN servers use this field to expose the head revision + * of the resource, which is useful + */ + const std::string& versionName() const + { return _versionName; } + + void setVersionName(const std::string& aVersion); + + DAVCollection* container() const + { return _container; } + + virtual bool isCollection() const + { return false; } + + void setVersionControlledConfiguration(const std::string& vcc); + const std::string& versionControlledConfiguration() const + { return _vcc; } + + void setMD5(const std::string& md5Hex); + const std::string& md5() const + { return _md5; } +protected: + friend class DAVCollection; + + Type _type; + std::string _url; + std::string _versionName; + std::string _vcc; + std::string _md5; + DAVCollection* _container; +}; + +typedef std::vector DAVResourceList; + +class DAVCollection : public DAVResource +{ +public: + DAVCollection(const std::string& url); + virtual ~DAVCollection(); + + DAVResourceList contents() const; + + void addChild(DAVResource* res); + void removeChild(DAVResource* res); + + DAVCollection* createChildCollection(const std::string& name); + + /** + * find the collection member with the specified URL, or return NULL + * if no such member of this collection exists. + */ + DAVResource* childWithUrl(const std::string& url) const; + + /** + * find the collection member with the specified name, or return NULL + */ + DAVResource* childWithName(const std::string& name) const; + + /** + * wrapper around URL manipulation + */ + std::string urlForChildWithName(const std::string& name) const; + + virtual bool isCollection() const + { return true; } +private: + DAVResourceList _contents; +}; + +class DAVMultiStatus +{ +public: + DAVMultiStatus(); + ~DAVMultiStatus(); + + // incremental XML parsing + void parseXML(const char* data, int size); + + void finishParse(); + + DAVResource* resource(); + + class DAVMultiStatusPrivate; +private: + std::auto_ptr _d; +}; + +} // of namespace simgear + +#endif // of SG_IO_DAVMULTISTATUS_HXX diff --git a/simgear/io/SVNDirectory.cxx b/simgear/io/SVNDirectory.cxx new file mode 100644 index 00000000..fa857f1c --- /dev/null +++ b/simgear/io/SVNDirectory.cxx @@ -0,0 +1,368 @@ + +#include "SVNDirectory.hxx" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::string; +using std::cout; +using std::endl; +using namespace simgear; + +typedef std::vector RequestVector; +typedef std::map DAVResourceMap; + + +const char* DAV_CACHE_NAME = ".terrasync_cache"; +const char* CACHE_VERSION_4_TOKEN = "terrasync-cache-4"; +const int MAX_UPDATE_REPORT_DEPTH = 3; + +enum LineState +{ + LINESTATE_HREF = 0, + LINESTATE_VERSIONNAME +}; + +SVNDirectory::SVNDirectory(SVNRepository *r, const SGPath& path) : + localPath(path), + dav(NULL), + repo(r), + _doingUpdateReport(false), + _parent(NULL) +{ + if (path.exists()) { + parseCache(); + } + + // don't create dir here, repo might not exist at all +} + +SVNDirectory::SVNDirectory(SVNDirectory* pr, DAVCollection* col) : + dav(col), + repo(pr->repository()), + _doingUpdateReport(false), + _parent(pr) +{ + assert(col->container()); + assert(!col->url().empty()); + assert(_parent); + + localPath = pr->fsDir().file(col->name()); + if (!localPath.exists()) { + Dir d(localPath); + d.create(0755); + writeCache(); + } else { + parseCache(); + } +} + +SVNDirectory::~SVNDirectory() +{ + // recursive delete our child directories + BOOST_FOREACH(SVNDirectory* d, _children) { + delete d; + } +} + +void SVNDirectory::parseCache() +{ + SGPath p(localPath); + p.append(DAV_CACHE_NAME); + if (!p.exists()) { + return; + } + + char href[1024]; + char versionName[128]; + LineState lineState = LINESTATE_HREF; + std::ifstream file(p.c_str()); + bool doneSelf = false; + + file.getline(href, 1024); + if (strcmp(CACHE_VERSION_4_TOKEN, href)) { + SG_LOG(SG_IO, SG_WARN, "invalid cache file:" << p.str()); + return; + } + + std::string vccUrl; + file.getline(href, 1024); + vccUrl = href; + + while (!file.eof()) { + if (lineState == LINESTATE_HREF) { + file.getline(href, 1024); + ++lineState; + } else { + assert(lineState == LINESTATE_VERSIONNAME); + file.getline(versionName, 1024); + lineState = LINESTATE_HREF; + char* hrefPtr = href; + + if (!doneSelf) { + if (!dav) { + dav = new DAVCollection(hrefPtr); + dav->setVersionName(versionName); + } else { + assert(string(hrefPtr) == dav->url()); + } + + if (!vccUrl.empty()) { + dav->setVersionControlledConfiguration(vccUrl); + } + + _cachedRevision = versionName; + doneSelf = true; + } else { + DAVResource* child = addChildDirectory(hrefPtr)->collection(); + child->setVersionName(versionName); + } + } // of line-state switching + } // of file get-line loop +} + +void SVNDirectory::writeCache() +{ + SGPath p(localPath); + if (!p.exists()) { + Dir d(localPath); + d.create(0755); + } + + p.append(DAV_CACHE_NAME); + + std::ofstream file(p.c_str(), std::ios::trunc); +// first, cache file version header + file << CACHE_VERSION_4_TOKEN << '\n'; + +// second, the repository VCC url + file << dav->versionControlledConfiguration() << '\n'; + +// third, our own URL, and version + file << dav->url() << '\n' << _cachedRevision << '\n'; + + BOOST_FOREACH(DAVResource* child, dav->contents()) { + if (child->isCollection()) { + file << child->name() << '\n' << child->versionName() << "\n"; + } + } // of child iteration +} + +void SVNDirectory::setBaseUrl(const string& url) +{ + if (_parent) { + SG_LOG(SG_IO, SG_ALERT, "setting base URL on non-root directory " << url); + return; + } + + if (dav && (url == dav->url())) { + return; + } + + dav = new DAVCollection(url); +} + +std::string SVNDirectory::url() const +{ + if (!_parent) { + return repo->baseUrl(); + } + + return _parent->url() + "/" + name(); +} + +std::string SVNDirectory::name() const +{ + return dav->name(); +} + +DAVResource* +SVNDirectory::addChildFile(const std::string& fileName) +{ + DAVResource* child = NULL; + child = new DAVResource(dav->urlForChildWithName(fileName)); + dav->addChild(child); + + writeCache(); + return child; +} + +SVNDirectory* +SVNDirectory::addChildDirectory(const std::string& dirName) +{ + if (dav->childWithName(dirName)) { + // existing child, let's remove it + deleteChildByName(dirName); + } + + DAVCollection* childCol = dav->createChildCollection(dirName); + SVNDirectory* child = new SVNDirectory(this, childCol); + _children.push_back(child); + writeCache(); + return child; +} + +void SVNDirectory::deleteChildByName(const std::string& nm) +{ + DAVResource* child = dav->childWithName(nm); + if (!child) { +// std::cerr << "ZZZ: deleteChildByName: unknown:" << nm << std::endl; + return; + } + + SGPath path = fsDir().file(nm); + dav->removeChild(child); + delete child; + + if (child->isCollection()) { + Dir d(path); + d.remove(true); + + DirectoryList::iterator it = findChildDir(nm); + if (it != _children.end()) { + SVNDirectory* c = *it; + // std::cout << "YYY: deleting SVNDirectory for:" << nm << std::endl; + delete c; + _children.erase(it); + } + } else { + path.remove(); + } + + writeCache(); +} + +void SVNDirectory::requestFailed(HTTP::Request *req) +{ + SG_LOG(SG_IO, SG_WARN, "Request failed for:" << req->url()); +} + +bool SVNDirectory::isDoingSync() const +{ + if (_doingUpdateReport) { + return true; + } + + BOOST_FOREACH(SVNDirectory* child, _children) { + if (child->isDoingSync()) { + return true; + } // of children + } + + return false; +} + +void SVNDirectory::beginUpdateReport() +{ + _doingUpdateReport = true; + _cachedRevision.clear(); + writeCache(); +} + +void SVNDirectory::updateReportComplete() +{ + _cachedRevision = dav->versionName(); + _doingUpdateReport = false; + writeCache(); + + SVNDirectory* pr = parent(); + if (pr) { + pr->writeCache(); + } +} + +SVNRepository* SVNDirectory::repository() const +{ + return repo; +} + +void SVNDirectory::mergeUpdateReportDetails(unsigned int depth, + string_list& items) +{ + // normal, easy case: we are fully in-sync at a revision + if (!_cachedRevision.empty()) { + std::ostringstream os; + os << "" + << repoPath() << ""; + items.push_back(os.str()); + return; + } + + Dir d(localPath); + if (depth >= MAX_UPDATE_REPORT_DEPTH) { + std::cerr << localPath << "exceeded MAX_UPDATE_REPORT_DEPTH, cleaning" << std::endl; + d.removeChildren(); + return; + } + + PathList cs = d.children(Dir::NO_DOT_OR_DOTDOT | Dir::INCLUDE_HIDDEN | Dir::TYPE_DIR); + BOOST_FOREACH(SGPath path, cs) { + SVNDirectory* c = child(path.file()); + if (!c) { + // ignore this child, if it's an incomplete download, + // it will be over-written on the update anyway + //std::cerr << "unknown SVN child" << path << std::endl; + } else { + // recurse down into children + c->mergeUpdateReportDetails(depth+1, items); + } + } // of child dir iteration +} + +std::string SVNDirectory::repoPath() const +{ + if (!_parent) { + return "/"; + } + + // find the length of the repository base URL, then + // trim that off our repo URL - job done! + size_t baseUrlLen = repo->baseUrl().size(); + return dav->url().substr(baseUrlLen + 1); +} + +SVNDirectory* SVNDirectory::parent() const +{ + return _parent; +} + +SVNDirectory* SVNDirectory::child(const std::string& dirName) const +{ + BOOST_FOREACH(SVNDirectory* d, _children) { + if (d->name() == dirName) { + return d; + } + } + + return NULL; +} + +DirectoryList::iterator +SVNDirectory::findChildDir(const std::string& dirName) +{ + DirectoryList::iterator it; + for (it=_children.begin(); it != _children.end(); ++it) { + if ((*it)->name() == dirName) { + return it; + } + } + return it; +} + +simgear::Dir SVNDirectory::fsDir() const +{ + return Dir(localPath); +} diff --git a/simgear/io/SVNDirectory.hxx b/simgear/io/SVNDirectory.hxx new file mode 100644 index 00000000..06876dd4 --- /dev/null +++ b/simgear/io/SVNDirectory.hxx @@ -0,0 +1,112 @@ +// DAVCollectionMirror.hxx - mirror a DAV collection to the local filesystem +// +// Copyright (C) 2013 James Turner +// +// 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. + + +#ifndef SG_IO_DAVCOLLECTIONMIRROR_HXX +#define SG_IO_DAVCOLLECTIONMIRROR_HXX + +#include +#include +#include + +#include +#include +#include + +namespace simgear { + +class Dir; +namespace HTTP { class Request; } + +// forward decls +class DAVMirror; +class SVNRepository; +class SVNDirectory; + +typedef std::vector DirectoryList; + +class SVNDirectory +{ +public: + // init from local + SVNDirectory(SVNRepository *repo, const SGPath& path); + ~SVNDirectory(); + + void setBaseUrl(const std::string& url); + + // init from a collection + SVNDirectory(SVNDirectory* pr, DAVCollection* col); + +// void update(); + // void gotResource(HTTP::Request* get, const std::string& etag); + void requestFailed(HTTP::Request* req); + + void beginUpdateReport(); + void updateReportComplete(); + + bool isDoingSync() const; + + std::string url() const; + + std::string name() const; + + DAVResource* addChildFile(const std::string& fileName); + SVNDirectory* addChildDirectory(const std::string& dirName); + + // void updateChild(DAVResource* child); + void deleteChildByName(const std::string& name); + + SGPath fsPath() const + { return localPath; } + + simgear::Dir fsDir() const; + + std::string repoPath() const; + + SVNRepository* repository() const; + DAVCollection* collection() const + { return dav; } + + std::string cachedRevision() const + { return _cachedRevision; } + + void mergeUpdateReportDetails(unsigned int depth, string_list& items); + + SVNDirectory* parent() const; + SVNDirectory* child(const std::string& dirName) const; +private: + + void parseCache(); + void writeCache(); + + DirectoryList::iterator findChildDir(const std::string& dirName); + + SGPath localPath; + DAVCollection* dav; + SVNRepository* repo; + + std::string _cachedRevision; + bool _doingUpdateReport; + + SVNDirectory* _parent; + DirectoryList _children; +}; + +} // of namespace simgear + +#endif // of SG_IO_DAVCOLLECTIONMIRROR_HXX diff --git a/simgear/io/SVNReportParser.cxx b/simgear/io/SVNReportParser.cxx new file mode 100644 index 00000000..f00973cf --- /dev/null +++ b/simgear/io/SVNReportParser.cxx @@ -0,0 +1,586 @@ +// SVNReportParser -- parser for SVN report XML data +// +// Copyright (C) 2012 James Turner +// +// 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. + +#include "SVNReportParser.hxx" + +#include +#include +#include +#include +#include +#include + +#include + +#include "simgear/misc/sg_path.hxx" +#include "simgear/misc/sg_dir.hxx" +#include "simgear/debug/logstream.hxx" +#include "simgear/xml/xmlparse.h" +#include "simgear/xml/easyxml.hxx" +#include "simgear/misc/strutils.hxx" +#include "simgear/package/md5.h" + +#include "SVNDirectory.hxx" +#include "SVNRepository.hxx" +#include "DAVMultiStatus.hxx" + +using std::cout; +using std::cerr; +using std::endl; +using std::string; + +using namespace simgear; + +#define DAV_NS "DAV::" +#define SVN_NS "svn::" +#define SUBVERSION_DAV_NS "http://subversion.tigris.org/xmlns/dav/" + +namespace { + + #define MAX_ENCODED_INT_LEN 10 + + static size_t + decode_size(unsigned char* &p, + const unsigned char *end) + { + if (p + MAX_ENCODED_INT_LEN < end) + end = p + MAX_ENCODED_INT_LEN; + /* Decode bytes until we're done. */ + size_t result = 0; + + while (p < end) { + result = (result << 7) | (*p & 0x7f); + if (((*p++ >> 7) & 0x1) == 0) { + break; + } + } + + return result; + } + + static bool + try_decode_size(unsigned char* &p, + const unsigned char *end) + { + if (p + MAX_ENCODED_INT_LEN < end) + end = p + MAX_ENCODED_INT_LEN; + + while (p < end) { + if (((*p++ >> 7) & 0x1) == 0) { + return true; + } + } + + return false; + } + +// const char* SVN_UPDATE_REPORT_TAG = SVN_NS "update-report"; + // const char* SVN_TARGET_REVISION_TAG = SVN_NS "target-revision"; + const char* SVN_OPEN_DIRECTORY_TAG = SVN_NS "open-directory"; + const char* SVN_OPEN_FILE_TAG = SVN_NS "open-file"; + const char* SVN_ADD_DIRECTORY_TAG = SVN_NS "add-directory"; + const char* SVN_ADD_FILE_TAG = SVN_NS "add-file"; + const char* SVN_TXDELTA_TAG = SVN_NS "txdelta"; + const char* SVN_SET_PROP_TAG = SVN_NS "set-prop"; + const char* SVN_DELETE_ENTRY_TAG = SVN_NS "delete-entry"; + + const char* SVN_DAV_MD5_CHECKSUM = SUBVERSION_DAV_NS ":md5-checksum"; + + const char* DAV_HREF_TAG = DAV_NS "href"; + const char* DAV_CHECKED_IN_TAG = SVN_NS "checked-in"; + + + const int svn_txdelta_source = 0; + const int svn_txdelta_target = 1; + const int svn_txdelta_new = 2; + + const size_t DELTA_HEADER_SIZE = 4; + + /** + * helper struct to decode and store the SVN delta header + * values + */ + struct SVNDeltaWindow + { + public: + + static bool isWindowComplete(unsigned char* buffer, size_t bytes) + { + unsigned char* p = buffer; + unsigned char* pEnd = p + bytes; + // if we can't decode five sizes, certainly incomplete + for (int i=0; i<5; i++) { + if (!try_decode_size(p, pEnd)) { + return false; + } + } + + p = buffer; + // ignore these three + decode_size(p, pEnd); + decode_size(p, pEnd); + decode_size(p, pEnd); + size_t instructionLen = decode_size(p, pEnd); + size_t newLength = decode_size(p, pEnd); + size_t headerLength = p - buffer; + + return (bytes >= (instructionLen + newLength + headerLength)); + } + + SVNDeltaWindow(unsigned char* p) : + headerLength(0), + _ptr(p) + { + sourceViewOffset = decode_size(p, p+20); + sourceViewLength = decode_size(p, p+20); + targetViewLength = decode_size(p, p+20); + instructionLength = decode_size(p, p+20); + newLength = decode_size(p, p+20); + + headerLength = p - _ptr; + _ptr = p; + + if (sourceViewOffset != 0) { + cout << "sourceViewOffset:" << sourceViewOffset << endl; + } + } + + bool apply(std::vector& output, std::istream& source) + { + unsigned char* pEnd = _ptr + instructionLength; + unsigned char* newData = pEnd; + + while (_ptr < pEnd) { + int op = ((*_ptr >> 6) & 0x3); + if (op >= 3) { + std::cerr << "weird opcode" << endl; + return false; + } + + int length = *_ptr++ & 0x3f; + int offset = 0; + + if (length == 0) { + length = decode_size(_ptr, pEnd); + } + + if (length == 0) { + std::cerr << "malformed stream, 0 length" << std::endl; + return false; + } + + // if op != new, decode another size value + if (op != svn_txdelta_new) { + offset = decode_size(_ptr, pEnd); + } + + if (op == svn_txdelta_target) { + while (length > 0) { + output.push_back(output[offset++]); + --length; + } + } else if (op == svn_txdelta_new) { + output.insert(output.end(), newData, newData + length); + } else if (op == svn_txdelta_source) { + source.seekg(offset); + char* sourceBuf = (char*) malloc(length); + assert(sourceBuf); + source.read(sourceBuf, length); + output.insert(output.end(), sourceBuf, sourceBuf + length); + free(sourceBuf); + } + } // of instruction loop + + return true; + } + + size_t size() const + { + return headerLength + instructionLength + newLength; + } + + unsigned int sourceViewOffset; + size_t sourceViewLength, + targetViewLength; + size_t headerLength, + instructionLength, + newLength; + +private: + unsigned char* _ptr; + }; + + +} // of anonymous namespace + +class SVNReportParser::SVNReportParserPrivate +{ +public: + SVNReportParserPrivate(SVNRepository* repo) : + tree(repo), + status(SVNRepository::NO_ERROR), + parserInited(false), + currentPath(repo->fsBase()) + { + inFile = false; + currentDir = repo->rootDir(); + } + + ~SVNReportParserPrivate() + { + } + + void startElement (const char * name, const char** attributes) + { + if (status != SVNRepository::NO_ERROR) { + return; + } + + ExpatAtts attrs(attributes); + tagStack.push_back(name); + if (!strcmp(name, SVN_TXDELTA_TAG)) { + txDeltaData.clear(); + } else if (!strcmp(name, SVN_ADD_FILE_TAG)) { + string fileName(attrs.getValue("name")); + SGPath filePath(currentDir->fsDir().file(fileName)); + currentPath = filePath; + inFile = true; + } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) { + string fileName(attrs.getValue("name")); + SGPath filePath(Dir(currentPath).file(fileName)); + currentPath = filePath; + + DAVResource* res = currentDir->collection()->childWithName(fileName); + if (!res || !filePath.exists()) { + // set error condition + } + + inFile = true; + } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) { + string dirName(attrs.getValue("name")); + Dir d(currentDir->fsDir().file(dirName)); + if (d.exists()) { + // policy decision : if we're doing an add, wipe the existing + d.remove(true); + } + + currentDir = currentDir->addChildDirectory(dirName); + currentPath = currentDir->fsPath(); + currentDir->beginUpdateReport(); + //cout << "addDir:" << currentPath << endl; + } else if (!strcmp(name, SVN_SET_PROP_TAG)) { + setPropName = attrs.getValue("name"); + setPropValue.clear(); + } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) { + md5Sum.clear(); + } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) { + string dirName; + if (attrs.getValue("name")) { + dirName = string(attrs.getValue("name")); + } + openDirectory(dirName); + } else if (!strcmp(name, SVN_DELETE_ENTRY_TAG)) { + string entryName(attrs.getValue("name")); + deleteEntry(entryName); + } else if (!strcmp(name, DAV_CHECKED_IN_TAG) || !strcmp(name, DAV_HREF_TAG)) { + // don't warn on these ones + } else { + //std::cerr << "unhandled element:" << name << std::endl; + } + } // of startElement + + void openDirectory(const std::string& dirName) + { + if (dirName.empty()) { + // root directory, we shall assume + currentDir = tree->rootDir(); + } else { + assert(currentDir); + currentDir = currentDir->child(dirName); + } + + assert(currentDir); + currentPath = currentDir->fsPath(); + currentDir->beginUpdateReport(); + } + + void deleteEntry(const std::string& entryName) + { + currentDir->deleteChildByName(entryName); + } + + bool decodeTextDelta(const SGPath& outputPath) + { + string decoded = strutils::decodeBase64(txDeltaData); + size_t bytesToDecode = decoded.size(); + std::vector output; + unsigned char* p = (unsigned char*) decoded.data(); + if (memcmp(decoded.data(), "SVN\0", DELTA_HEADER_SIZE) != 0) { + return false; // bad header + } + + bytesToDecode -= DELTA_HEADER_SIZE; + p += DELTA_HEADER_SIZE; + std::ifstream source; + source.open(outputPath.c_str(), std::ios::in | std::ios::binary); + + while (bytesToDecode > 0) { + SVNDeltaWindow window(p); + assert(bytesToDecode >= window.size()); + window.apply(output, source); + bytesToDecode -= window.size(); + p += window.size(); + } + + source.close(); + std::ofstream f; + f.open(outputPath.c_str(), + std::ios::out | std::ios::trunc | std::ios::binary); + f.write(output.data(), output.size()); + + // compute MD5 while we have the file in memory + MD5_CTX md5; + memset(&md5, 0, sizeof(MD5_CTX)); + MD5Init(&md5); + MD5Update(&md5, (unsigned char*) output.data(), output.size()); + MD5Final(&md5); + decodedFileMd5 = strutils::encodeHex(md5.digest, 16); + return true; + } + + void endElement (const char * name) + { + if (status != SVNRepository::NO_ERROR) { + return; + } + + assert(tagStack.back() == name); + tagStack.pop_back(); + if (!strcmp(name, SVN_TXDELTA_TAG)) { + if (!decodeTextDelta(currentPath)) { + fail(SVNRepository::ERROR_TXDELTA); + } + } else if (!strcmp(name, SVN_ADD_FILE_TAG)) { + finishFile(currentDir->addChildFile(currentPath.file())); + } else if (!strcmp(name, SVN_OPEN_FILE_TAG)) { + DAVResource* res = currentDir->collection()->childWithName(currentPath.file()); + assert(res); + finishFile(res); + } else if (!strcmp(name, SVN_ADD_DIRECTORY_TAG)) { + // pop directory + currentPath = currentPath.dir(); + currentDir->updateReportComplete(); + currentDir = currentDir->parent(); + } else if (!strcmp(name, SVN_SET_PROP_TAG)) { + if (setPropName == "svn:entry:committed-rev") { + revision = strutils::to_int(setPropValue); + currentVersionName = setPropValue; + if (!inFile) { + // for directories we have the resource already + // for adding files, we might not; we set the version name + // above when ending the add/open-file element + currentDir->collection()->setVersionName(currentVersionName); + } + } + } else if (!strcmp(name, SVN_DAV_MD5_CHECKSUM)) { + // validate against (presumably) just written file + if (decodedFileMd5 != md5Sum) { + fail(SVNRepository::ERROR_CHECKSUM); + } + } else if (!strcmp(name, SVN_OPEN_DIRECTORY_TAG)) { + if (currentDir->parent()) { + // pop the collection stack + currentDir = currentDir->parent(); + } + + currentDir->updateReportComplete(); + currentPath = currentDir->fsPath(); + } else { + // std::cout << "element:" << name; + } + } + + void finishFile(DAVResource* res) + { + res->setVersionName(currentVersionName); + res->setMD5(md5Sum); + currentPath = currentPath.dir(); + inFile = false; + } + + void data (const char * s, int length) + { + if (status != SVNRepository::NO_ERROR) { + return; + } + + if (tagStack.back() == SVN_SET_PROP_TAG) { + setPropValue += string(s, length); + } else if (tagStack.back() == SVN_TXDELTA_TAG) { + txDeltaData += string(s, length); + } else if (tagStack.back() == SVN_DAV_MD5_CHECKSUM) { + md5Sum += string(s, length); + } + } + + void pi (const char * target, const char * data) {} + + string tagN(const unsigned int n) const + { + int sz = tagStack.size(); + if (n >= sz) { + return string(); + } + + return tagStack[sz - (1 + n)]; + } + + void fail(SVNRepository::ResultCode err) + { + status = err; + } + + SVNRepository* tree; + DAVCollection* rootCollection; + SVNDirectory* currentDir; + SVNRepository::ResultCode status; + + bool parserInited; + XML_Parser xmlParser; + +// in-flight data + string_list tagStack; + string currentVersionName; + string txDeltaData; + SGPath currentPath; + bool inFile; + + unsigned int revision; + string md5Sum, decodedFileMd5; + std::string setPropName, setPropValue; +}; + + +//////////////////////////////////////////////////////////////////////// +// Static callback functions for Expat. +//////////////////////////////////////////////////////////////////////// + +#define VISITOR static_cast(userData) + +static void +start_element (void * userData, const char * name, const char ** atts) +{ + VISITOR->startElement(name, atts); +} + +static void +end_element (void * userData, const char * name) +{ + VISITOR->endElement(name); +} + +static void +character_data (void * userData, const char * s, int len) +{ + VISITOR->data(s, len); +} + +static void +processing_instruction (void * userData, + const char * target, + const char * data) +{ + VISITOR->pi(target, data); +} + +#undef VISITOR + +/////////////////////////////////////////////////////////////////////////////// + +SVNReportParser::SVNReportParser(SVNRepository* repo) : + _d(new SVNReportParserPrivate(repo)) +{ + +} + +SVNReportParser::~SVNReportParser() +{ +} + +SVNRepository::ResultCode +SVNReportParser::innerParseXML(const char* data, int size) +{ + if (_d->status != SVNRepository::NO_ERROR) { + return _d->status; + } + + bool isEnd = (data == NULL); + if (!XML_Parse(_d->xmlParser, data, size, isEnd)) { + SG_LOG(SG_IO, SG_INFO, "SVN parse error:" << XML_ErrorString(XML_GetErrorCode(_d->xmlParser)) + << " at line:" << XML_GetCurrentLineNumber(_d->xmlParser) + << " column " << XML_GetCurrentColumnNumber(_d->xmlParser)); + + XML_ParserFree(_d->xmlParser); + _d->parserInited = false; + return SVNRepository::ERROR_XML; + } else if (isEnd) { + XML_ParserFree(_d->xmlParser); + _d->parserInited = false; + } + + return _d->status; +} + +SVNRepository::ResultCode +SVNReportParser::parseXML(const char* data, int size) +{ + if (_d->status != SVNRepository::NO_ERROR) { + return _d->status; + } + + if (!_d->parserInited) { + _d->xmlParser = XML_ParserCreateNS(0, ':'); + XML_SetUserData(_d->xmlParser, _d.get()); + XML_SetElementHandler(_d->xmlParser, start_element, end_element); + XML_SetCharacterDataHandler(_d->xmlParser, character_data); + XML_SetProcessingInstructionHandler(_d->xmlParser, processing_instruction); + _d->parserInited = true; + } + + return innerParseXML(data, size); +} + +SVNRepository::ResultCode SVNReportParser::finishParse() +{ + if (_d->status != SVNRepository::NO_ERROR) { + return _d->status; + } + + return innerParseXML(NULL, 0); +} + +std::string SVNReportParser::etagFromRevision(unsigned int revision) +{ + // etags look like W/"7//", hopefully this is stable + // across different servers and similar + std::ostringstream os; + os << "W/\"" << revision << "//"; + return os.str(); +} + + diff --git a/simgear/io/SVNReportParser.hxx b/simgear/io/SVNReportParser.hxx new file mode 100644 index 00000000..1e94751b --- /dev/null +++ b/simgear/io/SVNReportParser.hxx @@ -0,0 +1,57 @@ +// SVNReportParser -- parser for SVN report XML data +// +// Copyright (C) 2012 James Turner +// +// 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. + + +#ifndef SG_IO_SVNREPORTPARSER_HXX +#define SG_IO_SVNREPORTPARSER_HXX + +#include +#include // for auto_ptr + +#include "SVNRepository.hxx" + +class SGPath; + +namespace simgear +{ + +class SVNRepository; + +class SVNReportParser +{ +public: + SVNReportParser(SVNRepository* repo); + ~SVNReportParser(); + + // incremental XML parsing + SVNRepository::ResultCode parseXML(const char* data, int size); + + SVNRepository::ResultCode finishParse(); + + static std::string etagFromRevision(unsigned int revision); + + class SVNReportParserPrivate; +private: + SVNRepository::ResultCode innerParseXML(const char* data, int size); + + std::auto_ptr _d; +}; + +} // of namespace simgear + +#endif // of SG_IO_SVNREPORTPARSER_HXX diff --git a/simgear/io/SVNRepository.cxx b/simgear/io/SVNRepository.cxx new file mode 100644 index 00000000..b8768eab --- /dev/null +++ b/simgear/io/SVNRepository.cxx @@ -0,0 +1,414 @@ +// DAVMirrorTree -- mirror a DAV tree to the local file-system +// +// Copyright (C) 2012 James Turner +// +// 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. + +#include "SVNRepository.hxx" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "simgear/debug/logstream.hxx" +#include "simgear/misc/strutils.hxx" +#include +#include +#include +#include +#include +#include + +using std::cout; +using std::cerr; +using std::endl; +using std::string; + +namespace simgear +{ + +typedef std::vector RequestVector; + +class SVNRepoPrivate +{ +public: + SVNRepoPrivate(SVNRepository* parent) : + p(parent), + isUpdating(false), + status(SVNRepository::NO_ERROR) + { ; } + + SVNRepository* p; // link back to outer + SVNDirectory* rootCollection; + HTTP::Client* http; + std::string baseUrl; + std::string vccUrl; + std::string targetRevision; + bool isUpdating; + SVNRepository::ResultCode status; + + void svnUpdateDone() + { + isUpdating = false; + } + + void updateFailed(HTTP::Request* req, SVNRepository::ResultCode err) + { + SG_LOG(SG_IO, SG_WARN, "SVN: failed to update from:" << req->url()); + isUpdating = false; + status = err; + } + + void propFindComplete(HTTP::Request* req, DAVCollection* col); + void propFindFailed(HTTP::Request* req, SVNRepository::ResultCode err); +}; + + +namespace { // anonmouse + + string makeAbsoluteUrl(const string& url, const string& base) + { + if (strutils::starts_with(url, "http://")) + return url; // already absolute + + assert(strutils::starts_with(base, "http://")); + int schemeEnd = base.find("://"); + int hostEnd = base.find('/', schemeEnd + 3); + if (hostEnd < 0) { + return url; + } + + return base.substr(0, hostEnd) + url; + } + + // keep the responses small by only requesting the properties we actually + // care about; the ETag, length and MD5-sum + const char* PROPFIND_REQUEST_BODY = + "" + "" + "" + "" + "" + "" + "" + ""; + + class PropFindRequest : public HTTP::Request + { + public: + PropFindRequest(SVNRepoPrivate* repo) : + Request(repo->baseUrl, "PROPFIND"), + _repo(repo) + { + } + + virtual string_list requestHeaders() const + { + string_list r; + r.push_back("Depth"); + return r; + } + + virtual string header(const string& name) const + { + if (name == "Depth") { + return "0"; + } + + return string(); + } + + virtual string requestBodyType() const + { + return "application/xml; charset=\"utf-8\""; + } + + virtual int requestBodyLength() const + { + return strlen(PROPFIND_REQUEST_BODY); + } + + virtual int getBodyData(char* buf, int count) const + { + int bodyLen = strlen(PROPFIND_REQUEST_BODY); + assert(count >= bodyLen); + memcpy(buf, PROPFIND_REQUEST_BODY, bodyLen); + return bodyLen; + } + + protected: + virtual void responseHeadersComplete() + { + if (responseCode() == 207) { + // fine + } else if (responseCode() == 404) { + _repo->propFindFailed(this, SVNRepository::ERROR_NOT_FOUND); + } else { + SG_LOG(SG_IO, SG_WARN, "request for:" << url() << + " return code " << responseCode()); + _repo->propFindFailed(this, SVNRepository::ERROR_SOCKET); + } + } + + virtual void responseComplete() + { + if (responseCode() == 207) { + _davStatus.finishParse(); + _repo->propFindComplete(this, (DAVCollection*) _davStatus.resource()); + } + } + + virtual void gotBodyData(const char* s, int n) + { + if (responseCode() != 207) { + return; + } + _davStatus.parseXML(s, n); + } + private: + SVNRepoPrivate* _repo; + DAVMultiStatus _davStatus; + }; + +class UpdateReportRequest : public HTTP::Request +{ +public: + UpdateReportRequest(SVNRepoPrivate* repo, + const std::string& aVersionName, + bool startEmpty) : + HTTP::Request("", "REPORT"), + _requestSent(0), + _parser(repo->p), + _repo(repo), + _failed(false) + { + setUrl(repo->vccUrl); + + _request = + "\n" + "\n" + "" + repo->baseUrl + "\n" + "unknown\n"; + + _request += "\n"; + + if (!startEmpty) { + string_list entries; + _repo->rootCollection->mergeUpdateReportDetails(0, entries); + BOOST_FOREACH(string e, entries) { + _request += e + "\n"; + } + } + + _request += ""; + } + + virtual string requestBodyType() const + { + return "application/xml; charset=\"utf-8\""; + } + + virtual int requestBodyLength() const + { + return _request.size(); + } + + virtual int getBodyData(char* buf, int count) const + { + int len = std::min(count, requestBodyLength() - _requestSent); + memcpy(buf, _request.c_str() + _requestSent, len); + _requestSent += len; + return len; + } + +protected: + virtual void responseHeadersComplete() + { + + } + + virtual void responseComplete() + { + if (_failed) { + return; + } + + if (responseCode() == 200) { + SVNRepository::ResultCode err = _parser.finishParse(); + if (err) { + _repo->updateFailed(this, err); + _failed = true; + } else { + _repo->svnUpdateDone(); + } + } else if (responseCode() == 404) { + _repo->updateFailed(this, SVNRepository::ERROR_NOT_FOUND); + _failed = true; + } else { + SG_LOG(SG_IO, SG_WARN, "SVN: request for:" << url() << + " return code " << responseCode()); + _repo->updateFailed(this, SVNRepository::ERROR_SOCKET); + _failed = true; + } + } + + virtual void gotBodyData(const char* s, int n) + { + if (_failed) { + return; + } + + if (responseCode() != 200) { + return; + } + + //cout << "body data:" << string(s, n) << endl; + SVNRepository::ResultCode err = _parser.parseXML(s, n); + if (err) { + _failed = true; + SG_LOG(SG_IO, SG_WARN, "SVN: request for:" << url() << + " XML parse failed"); + _repo->updateFailed(this, err); + } + } + + +private: + string _request; + mutable int _requestSent; + SVNReportParser _parser; + SVNRepoPrivate* _repo; + bool _failed; +}; + +} // anonymous + +SVNRepository::SVNRepository(const SGPath& base, HTTP::Client *cl) : + _d(new SVNRepoPrivate(this)) +{ + _d->http = cl; + _d->rootCollection = new SVNDirectory(this, base); + _d->baseUrl = _d->rootCollection->url(); +} + +SVNRepository::~SVNRepository() +{ + delete _d->rootCollection; +} + +void SVNRepository::setBaseUrl(const std::string &url) +{ + _d->baseUrl = url; + _d->rootCollection->setBaseUrl(url); +} + +std::string SVNRepository::baseUrl() const +{ + return _d->baseUrl; +} + +HTTP::Client* SVNRepository::http() const +{ + return _d->http; +} + +SGPath SVNRepository::fsBase() const +{ + return _d->rootCollection->fsPath(); +} + +bool SVNRepository::isBare() const +{ + if (!fsBase().exists() || Dir(fsBase()).isEmpty()) { + return true; + } + + if (_d->vccUrl.empty()) { + return true; + } + + return false; +} + +void SVNRepository::update() +{ + _d->status = NO_ERROR; + if (_d->targetRevision.empty() || _d->vccUrl.empty()) { + _d->isUpdating = true; + PropFindRequest* pfr = new PropFindRequest(_d.get()); + http()->makeRequest(pfr); + return; + } + + if (_d->targetRevision == rootDir()->cachedRevision()) { + SG_LOG(SG_IO, SG_DEBUG, baseUrl() << " in sync at version " << _d->targetRevision); + _d->isUpdating = false; + return; + } + + _d->isUpdating = true; + UpdateReportRequest* urr = new UpdateReportRequest(_d.get(), + _d->targetRevision, isBare()); + http()->makeRequest(urr); +} + +bool SVNRepository::isDoingSync() const +{ + if (_d->status != NO_ERROR) { + return false; + } + + return _d->isUpdating || _d->rootCollection->isDoingSync(); +} + +SVNDirectory* SVNRepository::rootDir() const +{ + return _d->rootCollection; +} + +SVNRepository::ResultCode +SVNRepository::failure() const +{ + return _d->status; +} + +/////////////////////////////////////////////////////////////////////////// + +void SVNRepoPrivate::propFindComplete(HTTP::Request* req, DAVCollection* c) +{ + targetRevision = c->versionName(); + vccUrl = makeAbsoluteUrl(c->versionControlledConfiguration(), baseUrl); + rootCollection->collection()->setVersionControlledConfiguration(vccUrl); + p->update(); +} + +void SVNRepoPrivate::propFindFailed(HTTP::Request *req, SVNRepository::ResultCode err) +{ + if (err != SVNRepository::ERROR_NOT_FOUND) { + SG_LOG(SG_IO, SG_WARN, "PropFind failed for:" << req->url()); + } + + isUpdating = false; + status = err; +} + +} // of namespace simgear diff --git a/simgear/io/SVNRepository.hxx b/simgear/io/SVNRepository.hxx new file mode 100644 index 00000000..535990c6 --- /dev/null +++ b/simgear/io/SVNRepository.hxx @@ -0,0 +1,76 @@ +// DAVMirrorTree.hxx - mirror a DAV tree to the local file system +// +// Copyright (C) 2012 James Turner +// +// 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. + + +#ifndef SG_IO_DAVMIRRORTREE_HXX +#define SG_IO_DAVMIRRORTREE_HXX + +#include +#include +#include + +#include + +namespace simgear { + + namespace HTTP { + class Client; + } + +class SVNDirectory; +class SVNRepoPrivate; + +class SVNRepository +{ +public: + + SVNRepository(const SGPath& root, HTTP::Client* cl); + ~SVNRepository(); + + SVNDirectory* rootDir() const; + SGPath fsBase() const; + + void setBaseUrl(const std::string& url); + std::string baseUrl() const; + + HTTP::Client* http() const; + + void update(); + + bool isDoingSync() const; + + enum ResultCode { + NO_ERROR = 0, + ERROR_NOT_FOUND, + ERROR_SOCKET, + ERROR_XML, + ERROR_TXDELTA, + ERROR_IO, + ERROR_CHECKSUM + }; + + ResultCode failure() const; +private: + bool isBare() const; + + std::auto_ptr _d; +}; + +} // of namespace simgear + +#endif // of SG_IO_DAVMIRRORTREE_HXX diff --git a/simgear/io/http_svn.cxx b/simgear/io/http_svn.cxx new file mode 100644 index 00000000..a56630e7 --- /dev/null +++ b/simgear/io/http_svn.cxx @@ -0,0 +1,49 @@ +#include +#include +#include + +#include +#include + + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace simgear; +using std::cout; +using std::endl; +using std::cerr; +using std::string; + +HTTP::Client* httpClient; + +int main(int argc, char* argv[]) +{ + sglog().setLogLevels( SG_ALL, SG_INFO ); + HTTP::Client cl; + httpClient = &cl; + + + SGPath p("/Users/jmt/Desktop/scenemodels"); + SVNRepository airports(p, &cl); + // airports.setBaseUrl("http://svn.goneabitbursar.com/testproject1"); + airports.setBaseUrl("http://terrascenery.googlecode.com/svn/trunk/data/Scenery/Models"); + +// airports.setBaseUrl("http://terrascenery.googlecode.com/svn/trunk/data/Scenery/Airports"); + airports.update(); + + while (airports.isDoingSync()) { + cl.update(100); + } + + cout << "all done!" << endl; + return EXIT_SUCCESS; +} \ No newline at end of file -- 2.39.5