387 lines
9.7 KiB
C++
387 lines
9.7 KiB
C++
// DAVMirrorTree -- mirror a DAV tree to the local file-system
|
|
//
|
|
// Copyright (C) 2012 James Turner <zakalawe@mac.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.
|
|
|
|
#include "SVNRepository.hxx"
|
|
|
|
#include <iostream>
|
|
#include <cstring>
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
#include <map>
|
|
#include <set>
|
|
#include <fstream>
|
|
#include <memory>
|
|
|
|
#include <boost/foreach.hpp>
|
|
|
|
#include "simgear/debug/logstream.hxx"
|
|
#include "simgear/misc/strutils.hxx"
|
|
#include <simgear/misc/sg_dir.hxx>
|
|
#include <simgear/io/HTTPClient.hxx>
|
|
#include <simgear/io/DAVMultiStatus.hxx>
|
|
#include <simgear/io/SVNDirectory.hxx>
|
|
#include <simgear/io/sg_file.hxx>
|
|
#include <simgear/io/SVNReportParser.hxx>
|
|
|
|
using std::cout;
|
|
using std::cerr;
|
|
using std::endl;
|
|
using std::string;
|
|
|
|
namespace simgear
|
|
{
|
|
|
|
typedef std::vector<HTTP::Request_ptr> RequestVector;
|
|
|
|
class SVNRepoPrivate
|
|
{
|
|
public:
|
|
SVNRepoPrivate(SVNRepository* parent) :
|
|
p(parent),
|
|
isUpdating(false),
|
|
status(SVNRepository::REPO_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_TERRASYNC, SG_WARN, "SVN: failed to update from:" << req->url()
|
|
<< "\n(repository:" << p->baseUrl() << ")");
|
|
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 =
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
|
|
"<D:propfind xmlns:D=\"DAV:\">"
|
|
"<D:prop xmlns:R=\"http://subversion.tigris.org/xmlns/dav/\">"
|
|
"<D:resourcetype/>"
|
|
"<D:version-name/>"
|
|
"<D:version-controlled-configuration/>"
|
|
"</D:prop>"
|
|
"</D:propfind>";
|
|
|
|
class PropFindRequest : public HTTP::Request
|
|
{
|
|
public:
|
|
PropFindRequest(SVNRepoPrivate* repo) :
|
|
Request(repo->baseUrl, "PROPFIND"),
|
|
_repo(repo)
|
|
{
|
|
assert(repo);
|
|
requestHeader("Depth") = "0";
|
|
setBodyData( PROPFIND_REQUEST_BODY,
|
|
"application/xml; charset=\"utf-8\"" );
|
|
}
|
|
|
|
protected:
|
|
virtual void responseHeadersComplete()
|
|
{
|
|
if (responseCode() == 207) {
|
|
// fine
|
|
} else if (responseCode() == 404) {
|
|
_repo->propFindFailed(this, SVNRepository::REPO_ERROR_NOT_FOUND);
|
|
} else {
|
|
SG_LOG(SG_TERRASYNC, SG_WARN, "request for:" << url() <<
|
|
" return code " << responseCode());
|
|
_repo->propFindFailed(this, SVNRepository::REPO_ERROR_SOCKET);
|
|
_repo = NULL;
|
|
}
|
|
|
|
Request::responseHeadersComplete();
|
|
}
|
|
|
|
virtual void onDone()
|
|
{
|
|
if (responseCode() == 207) {
|
|
_davStatus.finishParse();
|
|
if (_davStatus.isValid()) {
|
|
_repo->propFindComplete(this, (DAVCollection*) _davStatus.resource());
|
|
} else {
|
|
_repo->propFindFailed(this, SVNRepository::REPO_ERROR_SOCKET);
|
|
}
|
|
}
|
|
}
|
|
|
|
virtual void gotBodyData(const char* s, int n)
|
|
{
|
|
if (responseCode() != 207) {
|
|
return;
|
|
}
|
|
_davStatus.parseXML(s, n);
|
|
}
|
|
|
|
virtual void onFail()
|
|
{
|
|
HTTP::Request::onFail();
|
|
if (_repo) {
|
|
_repo->propFindFailed(this, SVNRepository::REPO_ERROR_SOCKET);
|
|
_repo = NULL;
|
|
}
|
|
}
|
|
|
|
private:
|
|
SVNRepoPrivate* _repo;
|
|
DAVMultiStatus _davStatus;
|
|
};
|
|
|
|
class UpdateReportRequest:
|
|
public HTTP::Request
|
|
{
|
|
public:
|
|
UpdateReportRequest(SVNRepoPrivate* repo,
|
|
const std::string& aVersionName,
|
|
bool startEmpty) :
|
|
HTTP::Request("", "REPORT"),
|
|
_parser(repo->p),
|
|
_repo(repo),
|
|
_failed(false)
|
|
{
|
|
setUrl(repo->vccUrl);
|
|
std::string request =
|
|
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
|
|
"<S:update-report send-all=\"true\" xmlns:S=\"svn:\">\n"
|
|
"<S:src-path>" + repo->baseUrl + "</S:src-path>\n"
|
|
"<S:depth>unknown</S:depth>\n"
|
|
"<S:entry rev=\"" + aVersionName + "\" depth=\"infinity\" start-empty=\"true\"/>\n";
|
|
|
|
if( !startEmpty )
|
|
{
|
|
string_list entries;
|
|
_repo->rootCollection->mergeUpdateReportDetails(0, entries);
|
|
BOOST_FOREACH(string e, entries)
|
|
{
|
|
request += e + "\n";
|
|
}
|
|
}
|
|
|
|
request += "</S:update-report>";
|
|
|
|
setBodyData(request, "application/xml; charset=\"utf-8\"");
|
|
}
|
|
|
|
protected:
|
|
virtual void onDone()
|
|
{
|
|
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::REPO_ERROR_NOT_FOUND);
|
|
_failed = true;
|
|
} else {
|
|
SG_LOG(SG_TERRASYNC, SG_WARN, "SVN: request for:" << url() <<
|
|
" got HTTP status " << responseCode());
|
|
_repo->updateFailed(this, SVNRepository::REPO_ERROR_HTTP);
|
|
_failed = true;
|
|
}
|
|
}
|
|
|
|
virtual void gotBodyData(const char* s, int n)
|
|
{
|
|
if (_failed) {
|
|
return;
|
|
}
|
|
|
|
if (responseCode() != 200) {
|
|
return;
|
|
}
|
|
|
|
SVNRepository::ResultCode err = _parser.parseXML(s, n);
|
|
if (err) {
|
|
_failed = true;
|
|
SG_LOG(SG_IO, SG_WARN, this << ": SVN: request for:" << url() << " failed:" << err);
|
|
_repo->updateFailed(this, err);
|
|
_repo = NULL;
|
|
}
|
|
}
|
|
|
|
virtual void onFail()
|
|
{
|
|
HTTP::Request::onFail();
|
|
if (_repo) {
|
|
_repo->updateFailed(this, SVNRepository::REPO_ERROR_SOCKET);
|
|
_repo = NULL;
|
|
}
|
|
}
|
|
private:
|
|
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 = REPO_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_TERRASYNC, 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 != REPO_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::REPO_ERROR_NOT_FOUND) {
|
|
SG_LOG(SG_TERRASYNC, SG_WARN, "PropFind failed for:" << req->url());
|
|
}
|
|
|
|
isUpdating = false;
|
|
status = err;
|
|
}
|
|
|
|
} // of namespace simgear
|