390 lines
9.2 KiB
C++
390 lines
9.2 KiB
C++
|
|
#include "SVNDirectory.hxx"
|
|
|
|
#include <cassert>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#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/SVNRepository.hxx>
|
|
#include <simgear/io/sg_file.hxx>
|
|
#include <simgear/io/SVNReportParser.hxx>
|
|
#include <simgear/package/md5.h>
|
|
#include <simgear/structure/exception.hxx>
|
|
|
|
using std::string;
|
|
using std::cout;
|
|
using std::endl;
|
|
using namespace simgear;
|
|
|
|
typedef std::vector<HTTP::Request_ptr> RequestVector;
|
|
typedef std::map<std::string, DAVResource*> DAVResourceMap;
|
|
|
|
|
|
const char* DAV_CACHE_NAME = ".terrasync_cache";
|
|
const char* CACHE_VERSION_4_TOKEN = "terrasync-cache-4";
|
|
|
|
// important: with the Google servers, setting this higher than '1' causes
|
|
// server internal errors (500, the connection is closed). In other words we
|
|
// can only specify update report items one level deep at most and no more.
|
|
// (the root and its direct children, not NOT grand-children)
|
|
const unsigned int MAX_UPDATE_REPORT_DEPTH = 1;
|
|
|
|
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());
|
|
if (!file.is_open()) {
|
|
SG_LOG(SG_TERRASYNC, SG_WARN, "unable to open cache file for reading:" << p);
|
|
return;
|
|
}
|
|
bool doneSelf = false;
|
|
|
|
file.getline(href, 1024);
|
|
if (strcmp(CACHE_VERSION_4_TOKEN, href)) {
|
|
SG_LOG(SG_TERRASYNC, SG_WARN, "invalid cache file [missing header token]:" << p << " '" << href << "'");
|
|
return;
|
|
}
|
|
|
|
std::string vccUrl;
|
|
file.getline(href, 1024);
|
|
vccUrl = href;
|
|
|
|
while (!file.eof()) {
|
|
if (lineState == LINESTATE_HREF) {
|
|
file.getline(href, 1024);
|
|
lineState = LINESTATE_VERSIONNAME;
|
|
} 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();
|
|
string s = strutils::strip(versionName);
|
|
if (!s.empty()) {
|
|
child->setVersionName(versionName);
|
|
}
|
|
} // of done self test
|
|
} // 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(string(DAV_CACHE_NAME) + ".new");
|
|
|
|
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
|
|
|
|
file.close();
|
|
|
|
// approximately atomic delete + rename operation
|
|
SGPath cacheName(localPath);
|
|
cacheName.append(DAV_CACHE_NAME);
|
|
p.rename(cacheName);
|
|
}
|
|
|
|
void SVNDirectory::setBaseUrl(const string& url)
|
|
{
|
|
if (_parent) {
|
|
SG_LOG(SG_TERRASYNC, 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);
|
|
childCol->setVersionName(child->cachedRevision());
|
|
_children.push_back(child);
|
|
writeCache();
|
|
return child;
|
|
}
|
|
|
|
void SVNDirectory::deleteChildByName(const std::string& nm)
|
|
{
|
|
DAVResource* child = dav->childWithName(nm);
|
|
if (!child) {
|
|
return;
|
|
}
|
|
|
|
SGPath path = fsDir().file(nm);
|
|
|
|
if (child->isCollection()) {
|
|
Dir d(path);
|
|
bool ok = d.remove(true);
|
|
if (!ok) {
|
|
SG_LOG(SG_TERRASYNC, SG_ALERT, "SVNDirectory::deleteChildByName: failed to remove dir:"
|
|
<< nm << " at path:\n\t" << path);
|
|
}
|
|
|
|
DirectoryList::iterator it = findChildDir(nm);
|
|
if (it != _children.end()) {
|
|
SVNDirectory* c = *it;
|
|
delete c;
|
|
_children.erase(it);
|
|
}
|
|
} else {
|
|
bool ok = path.remove();
|
|
if (!ok) {
|
|
SG_LOG(SG_TERRASYNC, SG_ALERT, "SVNDirectory::deleteChildByName: failed to remove path:" << nm
|
|
<< " at path:\n\t" << path);
|
|
}
|
|
}
|
|
|
|
dav->removeChild(child);
|
|
delete child;
|
|
|
|
writeCache();
|
|
}
|
|
|
|
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 << "<S:entry rev=\"" << _cachedRevision << "\" depth=\"infinity\">"
|
|
<< repoPath() << "</S:entry>";
|
|
items.push_back(os.str());
|
|
return;
|
|
}
|
|
|
|
Dir d(localPath);
|
|
if (depth >= MAX_UPDATE_REPORT_DEPTH) {
|
|
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);
|
|
}
|