diff --git a/CMakeLists.txt b/CMakeLists.txt index e406f6312..1f14a8e17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,6 +83,7 @@ endif() find_package(Sphinx) find_package(PdfLatex) find_package(QtKeychain) +find_package(Neon) set(WITH_QTKEYCHAIN ${QTKEYCHAIN_FOUND}) set(USE_INOTIFY ${INOTIFY_FOUND}) diff --git a/cmake/modules/FindNeon.cmake b/cmake/modules/FindNeon.cmake new file mode 100644 index 000000000..7d03e2c01 --- /dev/null +++ b/cmake/modules/FindNeon.cmake @@ -0,0 +1,73 @@ +# - Try to find Neon +# Once done this will define +# +# NEON_FOUND - system has Neon +# NEON_INCLUDE_DIRS - the Neon include directory +# NEON_LIBRARIES - Link these to use Neon +# NEON_DEFINITIONS - Compiler switches required for using Neon +# +# Copyright (c) 2011 Andreas Schneider +# +# Redistribution and use is allowed according to the terms of the New +# BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# + + +find_package(PkgConfig) +if (PKG_CONFIG_FOUND) + pkg_check_modules(_NEON neon) +endif (PKG_CONFIG_FOUND) + +include(GNUInstallDirs) + +find_path(NEON_INCLUDE_DIRS +NAMES + neon/ne_basic.h +HINTS + ${_NEON_INCLUDEDIR} + ${CMAKE_INSTALL_INCLUDEDIR} +) + +find_library(NEON_LIBRARIES +NAMES + neon +HINTS + ${_NEON_LIBDIR} + ${CMAKE_INSTALL_LIBDIR} + ${CMAKE_INSTALL_PREFIX}/lib + ${CMAKE_INSTALL_PREFIX}/lib64 +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Neon DEFAULT_MSG NEON_LIBRARIES NEON_INCLUDE_DIRS) + +# show the NEON_INCLUDE_DIRS and NEON_LIBRARIES variables only in the advanced view +mark_as_advanced(NEON_INCLUDE_DIRS NEON_LIBRARIES) + +# Check if neon was compiled with LFS support, if so, the NE_LFS variable has to +# be defined in the owncloud module. +# If neon was not compiled with LFS its also ok since the underlying system +# than probably supports large files anyway. +IF( CMAKE_FIND_ROOT_PATH ) + FIND_PROGRAM( NEON_CONFIG_EXECUTABLE NAMES neon-config HINTS ${CMAKE_FIND_ROOT_PATH}/bin ) +ELSE( CMAKE_FIND_ROOT_PATH ) + FIND_PROGRAM( NEON_CONFIG_EXECUTABLE NAMES neon-config ) +ENDIF( CMAKE_FIND_ROOT_PATH ) + +IF ( NEON_CONFIG_EXECUTABLE ) + MESSAGE(STATUS "neon-config executable: ${NEON_CONFIG_EXECUTABLE}") + # neon-config --support lfs + EXECUTE_PROCESS( COMMAND ${NEON_CONFIG_EXECUTABLE} "--support" "lfs" + RESULT_VARIABLE LFS + OUTPUT_STRIP_TRAILING_WHITESPACE ) + + IF (LFS EQUAL 0) + MESSAGE(STATUS "libneon has been compiled with LFS support") + SET(NEON_WITH_LFS 1 PARENT_SCOPE) + ELSE (LFS EQUAL 0) + MESSAGE(STATUS "libneon has not been compiled with LFS support, rely on OS") + ENDIF (LFS EQUAL 0) +ELSE ( NEON_CONFIG_EXECUTABLE ) + MESSAGE(STATUS, "neon-config could not be found.") +ENDIF ( NEON_CONFIG_EXECUTABLE ) diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 4c31a6134..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,153 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OwncloudDocumentation.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OwncloudDocumentation.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/OwncloudDocumentation" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OwncloudDocumentation" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5de879e2a..611e90f89 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,7 @@ set(3rdparty_HEADER ) qt_wrap_cpp(3rdparty_MOC ${3rdparty_HEADER}) -if(NOT WIN32) +if(NOT WIN32) list(APPEND 3rdparty_SRC 3rdparty/qtlockedfile/qtlockedfile_unix.cpp) else() list(APPEND 3rdparty_SRC 3rdparty/qtlockedfile/qtlockedfile_win.cpp ) @@ -50,6 +50,8 @@ set(libsync_SRCS mirall/networklocation.cpp mirall/mirallconfigfile.cpp mirall/csyncthread.cpp + mirall/owncloudpropagator.cpp + mirall/progressdatabase.cpp mirall/fileutils.cpp mirall/theme.cpp mirall/owncloudtheme.cpp @@ -78,6 +80,7 @@ set(libsync_HEADERS mirall/folder.h mirall/folderwatcher.h mirall/csyncthread.h + mirall/owncloudpropagator.h mirall/theme.h mirall/owncloudtheme.h mirall/owncloudinfo.h @@ -106,7 +109,7 @@ IF( INOTIFY_FOUND ) set(libsync_HEADERS ${libsync_HEADERS} mirall/inotify.h) set(libsync_HEADERS ${libsync_HEADERS} mirall/folderwatcher_inotify.h) ENDIF() -IF( WIN32 ) +IF( WIN32 ) set(libsync_SRCS ${libsync_SRCS} mirall/folderwatcher_win.cpp) set(libsync_HEADERS ${libsync_HEADERS} mirall/folderwatcher_win.h) ENDIF() @@ -117,10 +120,25 @@ ENDIF() qt_wrap_cpp(syncMoc ${libsync_HEADERS}) + +IF( DEFINED CSYNC_BUILD_PATH ) +IF(WIN32) +SET(HTTPBF_LIBRARY ${CSYNC_BUILD_PATH}/src/httpbf/libhttpbflib.dll) +ELSEIF( APPLE ) +SET(HTTPBF_LIBRARY ${CSYNC_BUILD_PATH}/src/httpbf/libhttpbflib.a) +ELSE() +SET(HTTPBF_LIBRARY ${CSYNC_BUILD_PATH}/src/httpbf/libhttpbflib.a) +ENDIF() +ELSE() +FIND_LIBRARY(HTTPBF_LIBRARY NAMES httpbflib HINTS $ENV{CSYNC_DIR}) +ENDIF() + + list(APPEND libsync_LINK_TARGETS ${QT_LIBRARIES} ${CSYNC_LIBRARY} - dl + ${NEON_LIBRARIES} + ${HTTPBF_LIBRARY} ) if(QTKEYCHAIN_FOUND) @@ -232,7 +250,7 @@ if( UNIX AND NOT APPLE) endif() # csync is required. -include_directories(${CSYNC_INCLUDE_DIR}/csync ${CSYNC_INCLUDE_DIR} ${CSYNC_BUILD_PATH}/src) +include_directories(${CSYNC_INCLUDE_DIR}/csync ${CSYNC_INCLUDE_DIR} ${CSYNC_INCLUDE_DIR}/httpbf/src ${CSYNC_BUILD_PATH}/src) include_directories(${3rdparty_INC}) qt_wrap_cpp(mirallMoc ${mirall_HEADERS}) @@ -312,6 +330,7 @@ set_target_properties( ${APPLICATION_EXECUTABLE} PROPERTIES target_link_libraries( ${APPLICATION_EXECUTABLE} ${QT_LIBRARIES} ) target_link_libraries( ${APPLICATION_EXECUTABLE} ${synclib_NAME} ) target_link_libraries( ${APPLICATION_EXECUTABLE} ${CSYNC_LIBRARY} ) +target_link_libraries( ${APPLICATION_EXECUTABLE} ${NEON_LIBRARIES} ) install(TARGETS ${APPLICATION_EXECUTABLE} RUNTIME DESTINATION bin @@ -337,3 +356,15 @@ if(KRAZY2_EXECUTABLE) ) endif() +set(OWNCLOUDCMD_SRC owncloudcmd/owncloudcmd.cpp) +add_executable(owncloudcmd ${OWNCLOUDCMD_SRC}) +set_target_properties( owncloudcmd PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${BIN_OUTPUT_DIRECTORY} ) +target_link_libraries(owncloudcmd owncloudsync) +target_link_libraries(owncloudcmd ${CSYNC_LIBRARY}) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/mirall) +install(TARGETS owncloudcmd + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +) diff --git a/src/mirall/csyncthread.cpp b/src/mirall/csyncthread.cpp index 68011ed5d..3c69a06d9 100644 --- a/src/mirall/csyncthread.cpp +++ b/src/mirall/csyncthread.cpp @@ -18,6 +18,8 @@ #include "mirall/theme.h" #include "mirall/logger.h" #include "mirall/owncloudinfo.h" +#include "owncloudpropagator.h" +#include "progressdatabase.h" #include "creds/abstractcredentials.h" #ifdef Q_OS_WIN @@ -47,11 +49,15 @@ namespace Mirall { QMutex CSyncThread::_mutex; QMutex CSyncThread::_syncMutex; -CSyncThread::CSyncThread(CSYNC *csync) +CSyncThread::CSyncThread(CSYNC *csync, const QString &localPath, const QString &remotePath) { _mutex.lock(); + _localPath = localPath; + _remotePath = remotePath; _csync_ctx = csync; _mutex.unlock(); + qRegisterMetaType("SyncFileItem"); + qRegisterMetaType("CSYNC_STATUS"); } CSyncThread::~CSyncThread() @@ -61,106 +67,106 @@ CSyncThread::~CSyncThread() //Convert an error code from csync to a user readable string. // Keep that function thread safe as it can be called from the sync thread or the main thread -QString CSyncThread::csyncErrorToString( CSYNC_ERROR_CODE err, const char *errString ) +QString CSyncThread::csyncErrorToString(CSYNC_STATUS err) { QString errStr; switch( err ) { - case CSYNC_ERR_NONE: + case CSYNC_STATUS_OK: errStr = tr("Success."); break; - case CSYNC_ERR_LOG: - errStr = tr("CSync Logging setup failed."); - break; - case CSYNC_ERR_LOCK: + case CSYNC_STATUS_NO_LOCK: errStr = tr("CSync failed to create a lock file."); break; - case CSYNC_ERR_STATEDB_LOAD: + case CSYNC_STATUS_STATEDB_LOAD_ERROR: errStr = tr("CSync failed to load the state db."); break; - case CSYNC_ERR_MODULE: + case CSYNC_STATUS_STATEDB_WRITE_ERROR: + errStr = tr("CSync failed to write the state db."); + break; + case CSYNC_STATUS_NO_MODULE: errStr = tr("

The %1 plugin for csync could not be loaded.
Please verify the installation!

").arg(Theme::instance()->appNameGUI()); break; - case CSYNC_ERR_TIMESKEW: + case CSYNC_STATUS_TIMESKEW: errStr = tr("The system time on this client is different than the system time on the server. " "Please use a time synchronization service (NTP) on the server and client machines " "so that the times remain the same."); break; - case CSYNC_ERR_FILESYSTEM: + case CSYNC_STATUS_FILESYSTEM_UNKNOWN: errStr = tr("CSync could not detect the filesystem type."); break; - case CSYNC_ERR_TREE: + case CSYNC_STATUS_TREE_ERROR: errStr = tr("CSync got an error while processing internal trees."); break; - case CSYNC_ERR_MEM: + case CSYNC_STATUS_MEMORY_ERROR: errStr = tr("CSync failed to reserve memory."); break; - case CSYNC_ERR_PARAM: + case CSYNC_STATUS_PARAM_ERROR: errStr = tr("CSync fatal parameter error."); break; - case CSYNC_ERR_UPDATE: + case CSYNC_STATUS_UPDATE_ERROR: errStr = tr("CSync processing step update failed."); break; - case CSYNC_ERR_RECONCILE: + case CSYNC_STATUS_RECONCILE_ERROR: errStr = tr("CSync processing step reconcile failed."); break; - case CSYNC_ERR_PROPAGATE: + case CSYNC_STATUS_PROPAGATE_ERROR: errStr = tr("CSync processing step propagate failed."); break; - case CSYNC_ERR_ACCESS_FAILED: + case CSYNC_STATUS_REMOTE_ACCESS_ERROR: errStr = tr("

The target directory does not exist.

Please check the sync setup.

"); break; - case CSYNC_ERR_REMOTE_CREATE: - case CSYNC_ERR_REMOTE_STAT: + case CSYNC_STATUS_REMOTE_CREATE_ERROR: + case CSYNC_STATUS_REMOTE_STAT_ERROR: errStr = tr("A remote file can not be written. Please check the remote access."); break; - case CSYNC_ERR_LOCAL_CREATE: - case CSYNC_ERR_LOCAL_STAT: + case CSYNC_STATUS_LOCAL_CREATE_ERROR: + case CSYNC_STATUS_LOCAL_STAT_ERROR: errStr = tr("The local filesystem can not be written. Please check permissions."); break; - case CSYNC_ERR_PROXY: + case CSYNC_STATUS_PROXY_ERROR: errStr = tr("CSync failed to connect through a proxy."); break; - case CSYNC_ERR_LOOKUP: + case CSYNC_STATUS_PROXY_AUTH_ERROR: + errStr = tr("CSync could not authenticate at the proxy."); + break; + case CSYNC_STATUS_LOOKUP_ERROR: errStr = tr("CSync failed to lookup proxy or server."); break; - case CSYNC_ERR_AUTH_SERVER: + case CSYNC_STATUS_SERVER_AUTH_ERROR: errStr = tr("CSync failed to authenticate at the %1 server.").arg(Theme::instance()->appNameGUI()); break; - case CSYNC_ERR_AUTH_PROXY: - errStr = tr("CSync failed to authenticate at the proxy."); - break; - case CSYNC_ERR_CONNECT: + case CSYNC_STATUS_CONNECT_ERROR: errStr = tr("CSync failed to connect to the network."); break; - case CSYNC_ERR_TIMEOUT: + case CSYNC_STATUS_TIMEOUT: errStr = tr("A network connection timeout happend."); break; - case CSYNC_ERR_HTTP: + case CSYNC_STATUS_HTTP_ERROR: errStr = tr("A HTTP transmission error happened."); break; - case CSYNC_ERR_PERM: - errStr = tr("CSync: Permission deniend."); + case CSYNC_STATUS_PERMISSION_DENIED: + errStr = tr("CSync failed due to not handled permission deniend."); break; - case CSYNC_ERR_NOT_FOUND: - errStr = tr("CSync: File not found."); + case CSYNC_STATUS_NOT_FOUND: + errStr = tr("CSync failed to find a specific file."); break; - case CSYNC_ERR_EXISTS: - errStr = tr("CSync: Directory already exists."); + case CSYNC_STATUS_FILE_EXISTS: + errStr = tr("CSync tried to create a directory that already exists."); break; - case CSYNC_ERR_NOSPC: - errStr = tr("CSync: No space left on %1 server.").arg(Theme::instance()->appNameGUI()); + case CSYNC_STATUS_OUT_OF_SPACE: + errStr = tr("CSync: No space on %1 server available.").arg(Theme::instance()->appNameGUI()); break; - case CSYNC_ERR_UNSPEC: - errStr = tr("CSync: unspecified error."); + case CSYNC_STATUS_QUOTA_EXCEEDED: + errStr = tr("CSync: No space on %1 server available.").arg(Theme::instance()->appNameGUI()); + break; + case CSYNC_STATUS_UNSUCCESSFUL: + errStr = tr("CSync unspecified error."); default: errStr = tr("An internal error number %1 happend.").arg( (int) err ); } - if( errString ) { - errStr += tr("
Backend Message: ")+QString::fromUtf8(errString); - } return errStr; } @@ -177,7 +183,7 @@ int CSyncThread::treewalkRemote( TREE_WALK_FILE* file, void *data ) int CSyncThread::walkFinalize(TREE_WALK_FILE* file, void *data ) { - return static_cast(data)->treewalkError( file); + return static_cast(data)->treewalkFinalize( file); } int CSyncThread::treewalkFile( TREE_WALK_FILE *file, bool remote ) @@ -185,11 +191,19 @@ int CSyncThread::treewalkFile( TREE_WALK_FILE *file, bool remote ) if( ! file ) return -1; SyncFileItem item; item._file = QString::fromUtf8( file->path ); + item._originalFile = file->path; item._instruction = file->instruction; item._dir = SyncFileItem::None; + if(file->error_string) { item._errorString = QString::fromUtf8(file->error_string); } + + item._isDirectory = file->type == CSYNC_FTW_TYPE_DIR; + item._modtime = file->modtime; + item._etag = file->md5; + // item._size = file->size; + SyncFileItem::Direction dir; int re = 0; @@ -215,6 +229,8 @@ int CSyncThread::treewalkFile( TREE_WALK_FILE *file, bool remote ) case CSYNC_INSTRUCTION_RENAME: dir = !remote ? SyncFileItem::Down : SyncFileItem::Up; item._renameTarget = QString::fromUtf8( file->rename_path ); + if (item._isDirectory) + _renamedFolders.insert(item._file, item._renameTarget); break; case CSYNC_INSTRUCTION_REMOVE: dir = !remote ? SyncFileItem::Down : SyncFileItem::Up; @@ -250,63 +266,49 @@ int CSyncThread::treewalkFile( TREE_WALK_FILE *file, bool remote ) } item._dir = dir; - _mutex.lock(); _syncedItems.append(item); - _mutex.unlock(); return re; } -int CSyncThread::treewalkError(TREE_WALK_FILE* file) +int CSyncThread::treewalkFinalize(TREE_WALK_FILE* file) { - SyncFileItem item; // only used for search. - item._file= QString::fromUtf8(file->path); - int indx = _syncedItems.indexOf(item); - - if ( indx == -1 ) + if (file->instruction == CSYNC_INSTRUCTION_IGNORE) return 0; - if( file && - (file->instruction == CSYNC_INSTRUCTION_STAT_ERROR || - file->instruction == CSYNC_INSTRUCTION_ERROR) ) { - _mutex.lock(); - _syncedItems[indx]._instruction = file->instruction; - _syncedItems[indx]._errorString = QString::fromUtf8(file->error_string); - _mutex.unlock(); - } + // Update the instruction and etag in the csync rb_tree so it is saved on the database + QHash::const_iterator action = _performedActions.constFind(file->path); + if (action != _performedActions.constEnd()) { + if (file->instruction != CSYNC_INSTRUCTION_NONE) { + // it is NONE if we are in the wrong tree (remote vs. local) + qDebug() << "UPDATING " << file->path << action->instruction; + + file->instruction = action->instruction; + } + + if (!action->etag.isNull()) { + // Update the etag even for INSTRUCTION_NONE (eg. renames) + file->md5 = action->etag.constData(); + } + } return 0; } -struct CSyncRunScopeHelper { - CSyncRunScopeHelper(CSYNC *ctx, CSyncThread *parent) - : _ctx(ctx), _parent(parent) - { - _t.start(); - } - ~CSyncRunScopeHelper() { - csync_commit(_ctx); - - qDebug() << "CSync run took " << _t.elapsed() << " Milliseconds"; - emit(_parent->finished()); - _parent->_syncMutex.unlock(); - } - CSYNC *_ctx; - QTime _t; - CSyncThread *_parent; -}; - void CSyncThread::handleSyncError(CSYNC *ctx, const char *state) { - CSYNC_ERROR_CODE err = csync_get_error( ctx ); - const char *errMsg = csync_get_error_string( ctx ); - QString errStr = csyncErrorToString(err, errMsg); + CSYNC_STATUS err = CSYNC_STATUS(csync_get_status( ctx )); + const char *errMsg = csync_get_status_string( ctx ); + QString errStr = csyncErrorToString(err); + if( errMsg ) { + errStr += QLatin1String("
"); + errStr += QString::fromUtf8(errMsg); + } qDebug() << " #### ERROR during "<< state << ": " << errStr; - switch (err) { - case CSYNC_ERR_SERVICE_UNAVAILABLE: - case CSYNC_ERR_CONNECT: + + if( CSYNC_STATUS_IS_EQUAL( err, CSYNC_STATUS_SERVICE_UNAVAILABLE ) || + CSYNC_STATUS_IS_EQUAL( err, CSYNC_STATUS_CONNECT_ERROR )) { emit csyncUnavailable(); - break; - default: + } else { emit csyncError(errStr); } } @@ -324,34 +326,16 @@ void CSyncThread::startSync() qDebug() << Q_FUNC_INFO << "Sync started"; qDebug() << "starting to sync " << qApp->thread() << QThread::currentThread(); + _syncedItems.clear(); _mutex.lock(); - _syncedItems.clear(); _needsUpdate = false; _mutex.unlock(); - // cleans up behind us and emits finished() to ease error handling - CSyncRunScopeHelper helper(_csync_ctx, this); // maybe move this somewhere else where it can influence a running sync? MirallConfigFile cfg; - int downloadLimit = 0; - if (cfg.useDownloadLimit()) { - downloadLimit = cfg.downloadLimit() * 1000; - } - csync_set_module_property(_csync_ctx, "bandwidth_limit_download", &downloadLimit); - - int uploadLimit = -75; // 75% - int useUpLimit = cfg.useUploadLimit(); - if ( useUpLimit >= 1) { - uploadLimit = cfg.uploadLimit() * 1000; - } else if (useUpLimit == 0) { - uploadLimit = 0; - } - csync_set_module_property(_csync_ctx, "bandwidth_limit_upload", &uploadLimit); - - csync_set_progress_callback( _csync_ctx, cb_progress ); csync_set_module_property(_csync_ctx, "csync_context", _csync_ctx); csync_set_userdata(_csync_ctx, this); @@ -378,18 +362,24 @@ void CSyncThread::startSync() + _syncTime.start(); + + QElapsedTimer updateTime; + updateTime.start(); qDebug() << "#### Update start #################################################### >>"; if( csync_update(_csync_ctx) < 0 ) { handleSyncError(_csync_ctx, "csync_update"); return; } - qDebug() << "<<#### Update end ###########################################################"; + qDebug() << "<<#### Update end #################################################### " << updateTime.elapsed(); if( csync_reconcile(_csync_ctx) < 0 ) { handleSyncError(_csync_ctx, "csync_reconcile"); return; } + _progressInfo = Progress::Info(); + _hasFiles = false; bool walkOk = true; if( csync_walk_local_tree(_csync_ctx, &treewalkLocal, 0) < 0 ) { @@ -400,9 +390,17 @@ void CSyncThread::startSync() qDebug() << "Error in remote treewalk."; } + // Adjust the paths for the renames. + for (SyncFileItemVector::iterator it = _syncedItems.begin(); + it != _syncedItems.end(); ++it) { + it->_file = adjustRenamedPath(it->_file); + } + + qSort(_syncedItems); + if (!_hasFiles && !_syncedItems.isEmpty()) { qDebug() << Q_FUNC_INFO << "All the files are going to be removed, asking the user"; - bool cancel = true; + bool cancel = false; emit aboutToRemoveAllFiles(_syncedItems.first()._dir, &cancel); if (cancel) { qDebug() << Q_FUNC_INFO << "Abort sync"; @@ -413,21 +411,135 @@ void CSyncThread::startSync() if (_needsUpdate) emit(started()); - if( csync_propagate(_csync_ctx) < 0 ) { - handleSyncError(_csync_ctx, "cysnc_reconcile"); - return; - } + ne_session_s *session = 0; + // that call to set property actually is a get which will return the session + csync_set_module_property(_csync_ctx, "get_dav_session", &session); + Q_ASSERT(session); - if( walkOk ) { - if( csync_walk_local_tree(_csync_ctx, &walkFinalize, 0) < 0 || - csync_walk_remote_tree(_csync_ctx, &walkFinalize, 0 ) < 0 ) { - qDebug() << "Error in finalize treewalk."; + _progressDataBase.load(_localPath); + _propagator.reset(new OwncloudPropagator (session, _localPath, _remotePath, &_progressDataBase)); + connect(_propagator.data(), SIGNAL(completed(SyncFileItem, CSYNC_STATUS)), + this, SLOT(transferCompleted(SyncFileItem, CSYNC_STATUS)), Qt::QueuedConnection); + connect(_propagator.data(), SIGNAL(progress(Progress::Kind,QString,quint64,quint64)), + this, SLOT(slotProgress(Progress::Kind,QString,quint64,quint64))); + _iterator = 0; + + int downloadLimit = 0; + if (cfg.useDownloadLimit()) { + downloadLimit = cfg.downloadLimit() * 1000; + } + _propagator->_downloadLimit = downloadLimit; + + int uploadLimit = -75; // 75% + int useUpLimit = cfg.useUploadLimit(); + if ( useUpLimit >= 1) { + uploadLimit = cfg.uploadLimit() * 1000; + } else if (useUpLimit == 0) { + uploadLimit = 0; + } + _propagator->_uploadLimit = uploadLimit; + + slotProgress(Progress::StartSync, QString(), 0, 0); + + startNextTransfer(); +} + +void CSyncThread::transferCompleted(const SyncFileItem &item, CSYNC_STATUS error) +{ + Action a; + a.instruction = item._instruction; + + // if the propagator had an error for a file, put the error string into the synced item + if( error != CSYNC_STATUS_OK + || a.instruction == CSYNC_INSTRUCTION_ERROR) { + + // Search for the item in the starting from _iterator because it should be a bit before it. + // This works because SyncFileItem::operator== only compare the file name; + int idx = _syncedItems.lastIndexOf(item, _iterator); + if (idx >= 0) { + _syncedItems[idx]._instruction = CSYNC_INSTRUCTION_ERROR; + _syncedItems[idx]._errorString = csyncErrorToString( error ); + _syncedItems[idx]._errorDetail = item._errorDetail; + _syncedItems[idx]._httpCode = item._httpCode; + qDebug() << "File " << item._file << " propagator error " << _syncedItems[idx]._errorString + << "(" << item._errorString << ")"; + } + + if (item._isDirectory && item._instruction == CSYNC_INSTRUCTION_REMOVE + && a.instruction == CSYNC_INSTRUCTION_DELETED) { + _lastDeleted = item._file; } else { - // emit the treewalk results. - emit treeWalkResult(_syncedItems); + _lastDeleted.clear(); } } - qDebug() << Q_FUNC_INFO << "Sync finished"; + + a.etag = item._etag; + _performedActions.insert(item._originalFile, a); + + if (item._instruction == CSYNC_INSTRUCTION_RENAME) { + if (a.instruction == CSYNC_INSTRUCTION_DELETED) { + // we should update the etag on the destination as well + a.instruction = CSYNC_INSTRUCTION_NONE; + } else { // ERROR + a.instruction = CSYNC_INSTRUCTION_ERROR; + } + _performedActions.insert(item._renameTarget.toUtf8(), a); + } + + if (!item._isDirectory && a.instruction == CSYNC_INSTRUCTION_UPDATED) { + slotProgress((item._dir != SyncFileItem::Up) ? Progress::EndDownload : Progress::EndUpload, + item._file, item._size, item._size); + _progressInfo.current_file_no++; + _progressInfo.overall_current_bytes += item._size; + } + + startNextTransfer(); +} + +void CSyncThread::startNextTransfer() +{ + while (_iterator < _syncedItems.size() && !_propagator->_hasFatalError) { + const SyncFileItem &item = _syncedItems.at(_iterator); + ++_iterator; + if (!_lastDeleted.isEmpty() && item._file.startsWith(_lastDeleted) + && item._instruction == CSYNC_INSTRUCTION_REMOVE) { + // If the item's name starts with the name of the previously deleted directory, we + // can assume this file was already destroyed by the previous recursive call. + Action a; + a.instruction = CSYNC_INSTRUCTION_DELETED; + _performedActions.insert(item._originalFile, a); + continue; + } + _propagator->_etag.clear(); // FIXME : set to the right one + + if (item._instruction == CSYNC_INSTRUCTION_SYNC || item._instruction == CSYNC_INSTRUCTION_NEW + || item._instruction == CSYNC_INSTRUCTION_CONFLICT) { + slotProgress((item._dir != SyncFileItem::Up) ? Progress::StartDownload : Progress::StartUpload, + item._file, 0, item._size); + } + + _propagator->propagate(item); + return; //propagate is async. + } + + // Everything is finished. + _progressDataBase.save(_localPath); + + if( csync_walk_local_tree(_csync_ctx, &walkFinalize, 0) < 0 || + csync_walk_remote_tree( _csync_ctx, &walkFinalize, 0 ) < 0 ) { + qDebug() << "Error in finalize treewalk."; + } else { + // emit the treewalk results. + emit treeWalkResult(_syncedItems); + } + + csync_commit(_csync_ctx); + + qDebug() << "CSync run took " << _syncTime.elapsed() << " Milliseconds"; + slotProgress(Progress::EndSync,QString(), 0 , 0); + emit finished(); + _propagator.reset(0); + _syncMutex.unlock(); } Progress::Kind CSyncThread::csyncToProgressKind( enum csync_notify_type_e kind ) @@ -475,33 +587,32 @@ Progress::Kind CSyncThread::csyncToProgressKind( enum csync_notify_type_e kind ) return pKind; } -void CSyncThread::cb_progress( CSYNC_PROGRESS *progress, void *userdata ) +void CSyncThread::slotProgress(Progress::Kind kind, const QString &file, quint64 curr, quint64 total) { - if( !progress ) { - qDebug() << "No progress block in progress callback found!"; - return; - } - if( !userdata ) { - qDebug() << "No thread given in progress callback!"; - return; - } - Progress::Info pInfo; - CSyncThread *thread = static_cast(userdata); + Progress::Info pInfo = _progressInfo; - pInfo.kind = thread->csyncToProgressKind( progress->kind ); - pInfo.current_file = QUrl::fromEncoded( progress->path ).toString(); - pInfo.file_size = progress->file_size; - pInfo.current_file_bytes = progress->curr_bytes; + pInfo.kind = kind; + pInfo.current_file = file; + pInfo.file_size = total; + pInfo.current_file_bytes = curr; - pInfo.overall_file_count = progress->overall_file_count; - pInfo.current_file_no = progress->current_file_no; - pInfo.overall_transmission_size = progress->overall_transmission_size; - pInfo.overall_current_bytes = progress->current_overall_bytes; + pInfo.overall_current_bytes += curr; pInfo.timestamp = QDateTime::currentDateTime(); // Connect to something in folder! - thread->transmissionProgress( pInfo ); - + transmissionProgress( pInfo ); } +/* Given a path on the remote, give the path as it is when the rename is done */ +QString CSyncThread::adjustRenamedPath(const QString& original) +{ + int slashPos = original.size(); + while ((slashPos = original.lastIndexOf('/' , slashPos - 1)) > 0) { + QHash< QString, QString >::const_iterator it = _renamedFolders.constFind(original.left(slashPos)); + if (it != _renamedFolders.constEnd()) { + return *it + original.mid(slashPos); + } + } + return original; +} } // ns Mirall diff --git a/src/mirall/csyncthread.h b/src/mirall/csyncthread.h index 14d8e0dd3..b92ea2c53 100644 --- a/src/mirall/csyncthread.h +++ b/src/mirall/csyncthread.h @@ -21,32 +21,45 @@ #include #include #include +#include #include #include #include #include "mirall/syncfileitem.h" +#include "progressdatabase.h" #include "mirall/progressdispatcher.h" class QProcess; +Q_DECLARE_METATYPE(CSYNC_STATUS) + namespace Mirall { +class OwncloudPropagator; + + class CSyncThread : public QObject { Q_OBJECT + + // Keep the actions that have been performed, in order to update the db + struct Action { + QByteArray etag; + csync_instructions_e instruction; + }; + QHash _performedActions; + public: - CSyncThread(CSYNC *); + CSyncThread(CSYNC *, const QString &localPath, const QString &remotePath); ~CSyncThread(); - static QString csyncErrorToString( CSYNC_ERROR_CODE, const char * ); + static QString csyncErrorToString( CSYNC_STATUS); Q_INVOKABLE void startSync(); signals: - void fileReceived( const QString& ); - void fileRemoved( const QString& ); void csyncError( const QString& ); void csyncWarning( const QString& ); void csyncUnavailable(); @@ -61,29 +74,48 @@ signals: void aboutToRemoveAllFiles(SyncFileItem::Direction direction, bool *cancel); +private slots: + void transferCompleted(const SyncFileItem& item, CSYNC_STATUS error); + void startNextTransfer(); + void slotProgress(Progress::Kind kind, const QString& file, quint64, quint64); + private: void handleSyncError(CSYNC *ctx, const char *state); - static void cb_progress( CSYNC_PROGRESS *progress, void *userdata ); - static int treewalkLocal( TREE_WALK_FILE*, void *); static int treewalkRemote( TREE_WALK_FILE*, void *); int treewalkFile( TREE_WALK_FILE*, bool ); - int treewalkError( TREE_WALK_FILE* ); + int treewalkFinalize( TREE_WALK_FILE* ); Progress::Kind csyncToProgressKind( enum csync_notify_type_e kind ); static int walkFinalize(TREE_WALK_FILE*, void* ); - static QMutex _mutex; static QMutex _syncMutex; SyncFileItemVector _syncedItems; + int _iterator; // index in _syncedItems for the next item to process. + ProgressDatabase _progressDataBase; + CSYNC *_csync_ctx; bool _needsUpdate; + QString _localPath; + QString _remotePath; + QScopedPointer _propagator; + QElapsedTimer _syncTime; + QString _lastDeleted; // if the last item was a path and it has been deleted + + + + // maps the origin and the target of the folders that have been renamed + QHash _renamedFolders; + QString adjustRenamedPath(const QString &original); bool _hasFiles; // true if there is at least one file that is not ignored or removed + Progress::Info _progressInfo; + int _downloadLimit; + int _uploadLimit; friend struct CSyncRunScopeHelper; }; diff --git a/src/mirall/folder.cpp b/src/mirall/folder.cpp index db354e149..a367e38ce 100644 --- a/src/mirall/folder.cpp +++ b/src/mirall/folder.cpp @@ -35,8 +35,7 @@ namespace Mirall { -void csyncLogCatcher(CSYNC */*ctx*/, - int /*verbosity*/, +void csyncLogCatcher(int /*verbosity*/, const char */*function*/, const char *buffer, void */*userdata*/) @@ -92,8 +91,8 @@ bool Folder::init() slotCSyncError(tr("Unable to create csync-context")); _csync_ctx = 0; } else { - csync_set_log_callback( _csync_ctx, csyncLogCatcher ); - csync_set_log_verbosity(_csync_ctx, 11); + csync_set_log_callback( csyncLogCatcher ); + csync_set_log_level( 11 ); MirallConfigFile cfgFile; csync_set_config_dir( _csync_ctx, cfgFile.configPath().toUtf8() ); @@ -103,8 +102,14 @@ bool Folder::init() cfgFile.getCredentials()->syncContextPreInit(_csync_ctx); if( csync_init( _csync_ctx ) < 0 ) { - qDebug() << "Could not initialize csync!" << csync_get_error(_csync_ctx) << csync_get_error_string(_csync_ctx); - slotCSyncError(CSyncThread::csyncErrorToString(csync_get_error(_csync_ctx), csync_get_error_string(_csync_ctx))); + qDebug() << "Could not initialize csync!" << csync_get_status(_csync_ctx) << csync_get_status_string(_csync_ctx); + QString errStr = CSyncThread::csyncErrorToString(CSYNC_STATUS(csync_get_status(_csync_ctx))); + const char *errMsg = csync_get_status_string(_csync_ctx); + if( errMsg ) { + errStr += QLatin1String("
"); + errStr += QString::fromUtf8(errMsg); + } + slotCSyncError(errStr); csync_destroy(_csync_ctx); _csync_ctx = 0; } @@ -563,7 +568,7 @@ void Folder::startSync(const QStringList &pathList) _thread = new QThread(this); _thread->setPriority(QThread::LowPriority); setIgnoredFiles(); - _csync = new CSyncThread( _csync_ctx ); + _csync = new CSyncThread( _csync_ctx, path(), QUrl(ownCloudInfo::instance()->webdavUrl() + secondPath()).path()); _csync->moveToThread(_thread); diff --git a/src/mirall/owncloudpropagator.cpp b/src/mirall/owncloudpropagator.cpp new file mode 100644 index 000000000..c5701ff27 --- /dev/null +++ b/src/mirall/owncloudpropagator.cpp @@ -0,0 +1,700 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Klaas Freitag + * + * 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. + */ + +#include "owncloudpropagator.h" +#include "progressdatabase.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// We use some internals of csync: +extern "C" int c_utimes(const char *, const struct timeval *); +extern "C" void csync_win32_set_file_hidden( const char *file, bool h ); + +namespace Mirall { + +/* Helper for QScopedPointer<>, to be used as the deleter. + * QScopePointer will call the right overload of cleanup for the pointer it holds + */ +struct ScopedPointerHelpers { + static inline void cleanup(hbf_transfer_t *pointer) { if (pointer) hbf_free_transfer(pointer); } + static inline void cleanup(ne_request *pointer) { if (pointer) ne_request_destroy(pointer); } + static inline void cleanup(ne_decompress *pointer) { if (pointer) ne_decompress_destroy(pointer); } +// static inline void cleanup(ne_propfind_handler *pointer) { if (pointer) ne_propfind_destroy(pointer); } +}; + +void OwncloudPropagator::propagate(const SyncFileItem &item) +{ + _errorCode = CSYNC_STATUS_OK; + _errorString.clear(); + _httpStatusCode = 0; + switch(item._instruction) { + case CSYNC_INSTRUCTION_REMOVE: + _instruction = item._dir == SyncFileItem::Down ? localRemove(item) : remoteRemove(item); + break; + case CSYNC_INSTRUCTION_NEW: + if (item._isDirectory) { + _instruction = item._dir == SyncFileItem::Down ? localMkdir(item) : remoteMkdir(item); + break; + } //fall trough + case CSYNC_INSTRUCTION_SYNC: + if (item._isDirectory) { + // Should we set the mtime? + _instruction = CSYNC_INSTRUCTION_UPDATED; + break; + } + _instruction = item._dir == SyncFileItem::Down ? downloadFile(item) : uploadFile(item); + break; + case CSYNC_INSTRUCTION_CONFLICT: + if (item._isDirectory) { + _instruction = CSYNC_INSTRUCTION_UPDATED; + break; + } + _instruction = downloadFile(item, true); + break; + case CSYNC_INSTRUCTION_RENAME: + _instruction = remoteRename(item); + break; + default: + _instruction = item._instruction; + break; + } + SyncFileItem newItem = item; + newItem._instruction = _instruction; + newItem._errorDetail = _errorString; + newItem._httpCode = _httpStatusCode; + newItem._etag = _etag; + emit completed(newItem, _errorCode); +} + +// compare two files with given filename and return true if they have the same content +static bool fileEquals(const QString &fn1, const QString &fn2) { + QFile f1(fn1); + QFile f2(fn2); + if (!f1.open(QIODevice::ReadOnly) || !f2.open(QIODevice::ReadOnly)) { + qDebug() << "fileEquals: Failed to open " << fn1 << "or" << fn2; + return false; + } + + if (f1.size() != f2.size()) { + return false; + } + + const int BufferSize = 16 * 1024; + char buffer1[BufferSize]; + char buffer2[BufferSize]; + do { + int r = f1.read(buffer1, BufferSize); + if (f2.read(buffer2, BufferSize) != r) { + // this should normaly not happen: the file are supposed to have the same size. + return false; + } + if (r <= 0) { + return true; + } + if (memcmp(buffer1, buffer2, r) != 0) { + return false; + } + } while (true); + return false; +} + +// Code copied from Qt5's QDir::removeRecursively +static bool removeRecursively(const QString &path) +{ + bool success = true; + QDirIterator di(path, QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot); + while (di.hasNext()) { + di.next(); + const QFileInfo& fi = di.fileInfo(); + bool ok; + if (fi.isDir() && !fi.isSymLink()) + ok = removeRecursively(di.filePath()); // recursive + else + ok = QFile::remove(di.filePath()); + if (!ok) + success = false; + } + if (success) + success = QDir().rmdir(path); + return success; +} + +csync_instructions_e OwncloudPropagator::localRemove(const SyncFileItem& item) +{ + QString filename = _localDir + item._file; + if (item._isDirectory) { + if (!QDir(filename).exists() || removeRecursively(filename)) + return CSYNC_INSTRUCTION_DELETED; + } else { + QFile file(filename); + if (!file.exists() || file.remove()) + return CSYNC_INSTRUCTION_DELETED; + _errorString = file.errorString(); + } + return CSYNC_INSTRUCTION_NONE; // not ERROR so it is still written to the database +} + +csync_instructions_e OwncloudPropagator::localMkdir(const SyncFileItem &item) +{ + QDir d; + if (!d.mkpath(_localDir + item._file)) { + _errorString = "could not create directory " + _localDir + item._file; + return CSYNC_INSTRUCTION_ERROR; + } + return CSYNC_INSTRUCTION_UPDATED; +} + +csync_instructions_e OwncloudPropagator::remoteRemove(const SyncFileItem &item) +{ + bool error = false; + + QScopedPointer uri(ne_path_escape((_remoteDir + item._file).toUtf8())); + int rc = ne_delete(_session, uri.data()); + + error = updateErrorFromSession(rc); + if (error) { + return CSYNC_INSTRUCTION_ERROR; + } + return CSYNC_INSTRUCTION_DELETED; +} + +csync_instructions_e OwncloudPropagator::remoteMkdir(const SyncFileItem &item) +{ + QScopedPointer uri(ne_path_escape((_remoteDir + item._file).toUtf8())); + bool error = false; + + int rc = ne_mkcol(_session, uri.data()); + error = updateErrorFromSession( rc ); + + if( error ) { + /* Special for mkcol: it returns 405 if the directory already exists. + * Ignre that error */ + if (_httpStatusCode != 405) { + return CSYNC_INSTRUCTION_ERROR; + } + } + return CSYNC_INSTRUCTION_UPDATED; +} + + +csync_instructions_e OwncloudPropagator::uploadFile(const SyncFileItem &item) +{ + QFile file(_localDir + item._file); + if (!file.open(QIODevice::ReadOnly)) { + _errorString = file.errorString(); + return CSYNC_INSTRUCTION_ERROR; + } + QScopedPointer uri(ne_path_escape((_remoteDir + item._file).toUtf8())); + + bool finished = true; + int attempts = 0; + /* + * do ten tries to upload the file chunked. Check the file size and mtime + * before submitting a chunk and after having submitted the last one. + * If the file has changed, retry. + */ + do { + Hbf_State state = HBF_SUCCESS; + QScopedPointer trans(hbf_init_transfer(uri.data())); + finished = true; + Q_ASSERT(trans); + state = hbf_splitlist(trans.data(), file.handle()); + + if (const ProgressDatabase::UploadInfo* progressInfo = _progressDb->getUploadInfo(item._file)) { + if (progressInfo->mtime == item._modtime) { + trans->start_id = progressInfo->chunk; + trans->transfer_id = progressInfo->transferid; + } + _progressDb->remove(item._file); + } + + ne_set_notifier(_session, notify_status_cb, this); + _lastTime.restart(); + _lastProgress = 0; + _chunked_done = 0; + _chunked_total_size = item._size; + _currentFile = item._file; + + if( state == HBF_SUCCESS ) { + _chunked_total_size = trans->stat_size; + /* Transfer all the chunks through the HTTP session using PUT. */ + state = hbf_transfer( _session, trans.data(), "PUT" ); + } + + /* Handle errors. */ + if ( state != HBF_SUCCESS ) { + + /* If the source file changed during submission, lets try again */ + if( state == HBF_SOURCE_FILE_CHANGE ) { + if( attempts++ < 30 ) { /* FIXME: How often do we want to try? */ + finished = false; /* make it try again from scratch. */ + qDebug("SOURCE file has changed during upload, retry #%d in two seconds!", attempts); + sleep(2); + } + } + + if( finished ) { + _errorString = hbf_error_string(trans.data(), state); + _httpStatusCode = hbf_fail_http_code(trans.data()); + + if (trans->start_id > 0) { + ProgressDatabase::UploadInfo pi; + pi.chunk = trans->start_id; + pi.transferid = trans->transfer_id; + pi.mtime = item._modtime; + _progressDb->setUploadInfo(item._file, pi); + } + return CSYNC_INSTRUCTION_ERROR; + } + } + } while( !finished ); + + ne_set_notifier(_session, 0, 0); + + updateMTimeAndETag(uri.data(), item._modtime); + + return CSYNC_INSTRUCTION_UPDATED; +} + +static QByteArray parseEtag(ne_request *req) { + const char *header = ne_get_response_header(req, "etag"); + if(header && header [0] == '"' && header[ strlen(header)-1] == '"') { + return QByteArray(header + 1, strlen(header)-2); + } else { + return header; + } +} + +void OwncloudPropagator::updateMTimeAndETag(const char* uri, time_t mtime) +{ + QByteArray modtime = QByteArray::number(qlonglong(mtime)); + ne_propname pname; + pname.nspace = "DAV:"; + pname.name = "lastmodified"; + ne_proppatch_operation ops[2]; + ops[0].name = &pname; + ops[0].type = ne_propset; + ops[0].value = modtime.constData(); + ops[1].name = NULL; + + int rc = ne_proppatch( _session, uri, ops ); + bool error = updateErrorFromSession( rc ); + if( error ) { + // FIXME: We could not set the mtime. Error or not? + qDebug() << "PROP-Patching of modified date failed."; + } + + // get the etag + QScopedPointer req(ne_request_create(_session, "HEAD", uri)); + int neon_stat = ne_request_dispatch(req.data()); + + if( updateErrorFromSession(neon_stat, req.data()) ) { + // error happend + qDebug() << "Could not issue HEAD request for ETag."; + } else { + _etag = parseEtag(req.data()); + } +} + +class DownloadContext { + +public: + QFile *_file; + QScopedPointer _decompress; + + explicit DownloadContext(QFile *file) : _file(file) {} + + static int content_reader(void *userdata, const char *buf, size_t len) + { + DownloadContext *writeCtx = static_cast(userdata); + size_t written = 0; + + if(buf) { + written = writeCtx->_file->write(buf, len); + if( len != written ) { + qDebug("WRN: content_reader wrote wrong num of bytes: %zu, %zu", len, written); + } + return NE_OK; + } + return NE_ERROR; + } + + + /* + * This hook is called after the response is here from the server, but before + * the response body is parsed. It decides if the response is compressed and + * if it is it installs the compression reader accordingly. + * If the response is not compressed, the normal response body reader is installed. + */ + static void install_content_reader( ne_request *req, void *userdata, const ne_status *status ) + { + DownloadContext *writeCtx = static_cast(userdata); + + Q_UNUSED(status); + + if( !writeCtx ) { + qDebug("Error: install_content_reader called without valid write context!"); + return; + } + + const char *enc = ne_get_response_header( req, "Content-Encoding" ); + qDebug("Content encoding ist <%s> with status %d", enc ? enc : "empty", + status ? status->code : -1 ); + + if( enc == QLatin1String("gzip") ) { + writeCtx->_decompress.reset(ne_decompress_reader( req, ne_accept_2xx, + content_reader, /* reader callback */ + writeCtx )); /* userdata */ + } else { + ne_add_response_body_reader( req, ne_accept_2xx, + content_reader, + (void*) writeCtx ); + } + } +}; + +csync_instructions_e OwncloudPropagator::downloadFile(const SyncFileItem &item, bool isConflict) +{ + QString tmpFileName; + const ProgressDatabase::DownloadInfo* progressInfo = _progressDb->getDownloadInfo(item._file); + if (progressInfo) { + if (progressInfo->etag != item._etag) { + QFile::remove(_localDir + progressInfo->tmpfile); + } else { + tmpFileName = progressInfo->tmpfile; + } + _progressDb->remove(item._file); + } + if (tmpFileName.isEmpty()) { + tmpFileName = item._file; + //add a dot at the begining of the filename to hide the file. + int slashPos = tmpFileName.lastIndexOf('/'); + tmpFileName.insert(slashPos+1, '.'); + //add the suffix + tmpFileName += ".~" + QString::number(uint(qrand()), 16); + } + + QFile tmpFile(_localDir + tmpFileName); + if (!tmpFile.open(QIODevice::Append)) { + _errorString = tmpFile.errorString(); + _errorCode = CSYNC_STATUS_LOCAL_CREATE_ERROR; + return CSYNC_INSTRUCTION_ERROR; + } + + csync_win32_set_file_hidden(tmpFileName.toUtf8().constData(), true); + + { + ProgressDatabase::DownloadInfo pi; + pi.etag = item._etag; + pi.tmpfile = tmpFileName; + _progressDb->setDownloadInfo(item._file, pi); + _progressDb->save(_localDir); + } + + /* actually do the request */ + int retry = 0; + + QScopedPointer uri(ne_path_escape((_remoteDir + item._file).toUtf8())); + DownloadContext writeCtx(&tmpFile); + + do { + QScopedPointer req(ne_request_create(_session, "GET", uri.data())); + + /* Allow compressed content by setting the header */ + ne_add_request_header( req.data(), "Accept-Encoding", "gzip" ); + + if (tmpFile.size() > 0) { + char brange[64]; + ne_snprintf(brange, sizeof brange, "bytes=%lld-", (long long) tmpFile.size()); + ne_add_request_header(req.data(), "Range", brange); + ne_add_request_header(req.data(), "Accept-Ranges", "bytes"); + qDebug("Retry with range %s", brange); + } + + /* hook called before the content is parsed to set the correct reader, + * either the compressed- or uncompressed reader. + */ + ne_hook_post_headers( _session, DownloadContext::install_content_reader, &writeCtx); + ne_set_notifier(_session, notify_status_cb, this); + _lastProgress = 0; + _lastTime.start(); + _chunked_done = _chunked_total_size = 0; + _currentFile = item._file; + + int neon_stat = ne_request_dispatch(req.data()); + + if (neon_stat == NE_TIMEOUT && (++retry) < 3) { + continue; + } + + /* delete the hook again, otherwise they get chained as they are with the session */ + ne_unhook_post_headers( _session, DownloadContext::install_content_reader, &writeCtx ); + ne_set_notifier(_session, 0, 0); + _chunked_done = _chunked_total_size = 0; + + if( updateErrorFromSession(neon_stat, req.data() ) ) { + qDebug("Error GET: Neon: %d", neon_stat); + if (tmpFile.size() == 0) { + // don't keep the temporary file if it is empty. + tmpFile.close(); + tmpFile.remove(); + _progressDb->remove(item._file); + } + return CSYNC_INSTRUCTION_ERROR; + } + + _etag = parseEtag(req.data()); + break; + } while (1); + + + tmpFile.close(); + tmpFile.flush(); + + //In case of conflict, make a backup of the old file + if (isConflict) { + QString fn = _localDir + item._file; + + // compare the files to see if there was an actual conflict. + if (fileEquals(fn, tmpFile.fileName())) { + tmpFile.remove(); + _progressDb->remove(item._file); + return CSYNC_INSTRUCTION_UPDATED; + } + + QFile f(fn); + // Add _conflict-XXXX before the extention. + int dotLocation = fn.lastIndexOf('.'); + // If no extention, add it at the end (take care of cases like foo/.hidden or foo.bar/file) + if (dotLocation <= fn.lastIndexOf('/') + 1) { + dotLocation = fn.size(); + } + fn.insert(dotLocation, "_conflict-" + QDateTime::fromTime_t(item._modtime).toString("yyyyMMdd-hhmmss")); + if (!f.rename(fn)) { + //If the rename fails, don't replace it. + _errorString = f.errorString(); + return CSYNC_INSTRUCTION_ERROR; + } + } + + csync_win32_set_file_hidden(tmpFileName.toUtf8().constData(), false); + + // We want a rename that also overwite. QFile::rename does not overwite. + // Qt 5.1 has QFile::renameOverwrite we cold use. (Or even better: QSaveFile) +#ifndef QT_OS_WIN + if (!tmpFile.fileEngine()->rename(_localDir + item._file)) { + _errorString = tmpFile.errorString(); + return CSYNC_INSTRUCTION_ERROR; + } +#else //QT_OS_WIN + if (::MoveFileEx((wchar_t*)tmpFile.fileName().utf16(), + (wchar_t*)QString(_localDir + item._file).utf16(), + MOVEFILE_REPLACE_EXISTING) != 0) { + wchar_t *string = 0; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM, + NULL, ::GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&string, 0, NULL); + _errorString = QString::fromWCharArray(string); + LocalFree((HLOCAL)string); + return CSYNC_INSTRUCTION_ERROR; + } +#endif + + _progressDb->remove(item._file); + + struct timeval times[2]; + times[0].tv_sec = times[1].tv_sec = item._modtime; + times[0].tv_usec = times[1].tv_usec = 0; + c_utimes((_localDir + item._file).toUtf8().data(), times); + + return CSYNC_INSTRUCTION_UPDATED; +} + +csync_instructions_e OwncloudPropagator::remoteRename(const SyncFileItem &item) +{ + if (item._file == item._renameTarget) + return CSYNC_INSTRUCTION_DELETED; // nothing to do; + QScopedPointer uri1(ne_path_escape((_remoteDir + item._file).toUtf8())); + QScopedPointer uri2(ne_path_escape((_remoteDir + item._renameTarget).toUtf8())); + + + int rc = ne_move(_session, 1, uri1.data(), uri2.data()); + if (updateErrorFromSession(rc)) { + // We set the instruction to UPDATED so next try we try to rename again + return CSYNC_INSTRUCTION_UPDATED; + } + + updateMTimeAndETag(uri2.data(), item._modtime); + + return CSYNC_INSTRUCTION_DELETED; +} + +bool OwncloudPropagator::check_neon_session() +{ + bool isOk = true; + if( !_session ) { + _errorCode = CSYNC_STATUS_PARAM_ERROR; + isOk = false; + } else { + const char *p = ne_get_error( _session ); + _errorString = QString::fromUtf8(p); + + if( !_errorString.isEmpty() ) { + int firstSpace = _errorString.indexOf(QChar(' ')); + if( firstSpace > 0 ) { + bool ok; + QString numStr = _errorString.mid(0, firstSpace); + _httpStatusCode = numStr.toInt(&ok); + + if( !ok ) { + _httpStatusCode = 0; + } + } + isOk = false; + } + } + return isOk; +} + +// returns true in case there was an error +bool OwncloudPropagator::updateErrorFromSession(int neon_code, ne_request *req) +{ + bool re = false; + + if( neon_code != NE_OK ) { + qDebug("Neon error code was %d", neon_code); + re = true; // there was an error. + } + + switch(neon_code) { + case NE_OK: /* Success, but still the possiblity of problems */ + if( req != NULL ) { + const ne_status *status = ne_get_status(req); + if( status ) { + if( status->klass != 2 ) { + _httpStatusCode = status->code; + _errorCode = CSYNC_STATUS_HTTP_ERROR; + _errorString = QString::fromUtf8( status->reason_phrase ); + re = true; + } + } else { + re = true; // can not get the status + } + } else { + // no neon request available. + re = check_neon_session(); + } + break; + case NE_ERROR: /* Generic error; use ne_get_error(session) for message */ + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_HTTP_ERROR; + break; + case NE_LOOKUP: /* Server or proxy hostname lookup failed */ + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_LOOKUP_ERROR; + _hasFatalError = true; + break; + case NE_AUTH: /* User authentication failed on server */ + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_REMOTE_ACCESS_ERROR; + _hasFatalError = true; + break; + case NE_PROXYAUTH: /* User authentication failed on proxy */ + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_PROXY_AUTH_ERROR; + _hasFatalError = true; + break; + case NE_CONNECT: /* Could not connect to server */ + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_CONNECT_ERROR; + _hasFatalError = true; + break; + case NE_TIMEOUT: /* Connection timed out */ + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_TIMEOUT; + _hasFatalError = true; + break; + case NE_FAILED: /* The precondition failed */ + case NE_RETRY: /* Retry request (ne_end_request ONLY) */ + case NE_REDIRECT: /* See ne_redirect.h */ + default: + _errorString = QString::fromUtf8( ne_get_error(_session) ); + _errorCode = CSYNC_STATUS_HTTP_ERROR; + break; + } + return re; +} + +void OwncloudPropagator::notify_status_cb(void* userdata, ne_session_status status, + const ne_session_status_info* info) +{ + OwncloudPropagator* this_ = reinterpret_cast(userdata); + + if ((status == ne_status_sending || status == ne_status_recving)) { + if (info->sr.total > 0) { + emit this_->progress(Progress::Context, this_->_currentFile, + this_->_chunked_done + info->sr.progress, + this_->_chunked_total_size ? this_->_chunked_total_size : info->sr.total ); + } + if (this_->_chunked_total_size && info->sr.total > 0 && info->sr.total == info->sr.progress) { + this_->_chunked_done += info->sr.total; + } + } + + /* throttle connection */ + int bandwidth_limit = 0; + if (status == ne_status_sending) bandwidth_limit = this_->_uploadLimit; + if (status == ne_status_recving) bandwidth_limit = this_->_downloadLimit; + if (bandwidth_limit > 0) { + int64_t diff = this_->_lastTime.nsecsElapsed() / 1000; + int64_t len = info->sr.progress - this_->_lastProgress; + if (len > 0 && diff > 0 && (1000000 * len / diff) > (int64_t)bandwidth_limit) { + int64_t wait_time = (1000000 * len / bandwidth_limit) - diff; + if (wait_time > 0) { + usleep(wait_time); + } + } + this_->_lastProgress = info->sr.progress; + this_->_lastTime.start(); + } else if (bandwidth_limit < 0 && bandwidth_limit > -100) { + int64_t diff = this_->_lastTime.nsecsElapsed() / 1000; + if (diff > 0) { + // -bandwidth_limit is the % of bandwidth + int64_t wait_time = -diff * (1 + 100.0 / bandwidth_limit); + if (wait_time > 0) { + usleep(wait_time); + } + } + this_->_lastTime.start(); + } +} + + +} diff --git a/src/mirall/owncloudpropagator.h b/src/mirall/owncloudpropagator.h new file mode 100644 index 000000000..11547bbe6 --- /dev/null +++ b/src/mirall/owncloudpropagator.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) by Olivier Goffart + * + * 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. + */ + +#ifndef OWNCLOUDPROPAGATOR_H +#define OWNCLOUDPROPAGATOR_H + +#include +#include +#include +#include + +#include "syncfileitem.h" +#include "progressdispatcher.h" + +struct ne_session_s; +struct ne_decompress_s; + +namespace Mirall { + +class ProgressDatabase; + +class OwncloudPropagator : public QObject { + Q_OBJECT + + ne_session_s *_session; + QString _localDir; // absolute path to the local directory. ends with '/' + QString _remoteDir; // path to the root of the remote. ends with '/' + ProgressDatabase *_progressDb; + + QString _errorString; + CSYNC_STATUS _errorCode; + int _httpStatusCode; + csync_instructions_e _instruction; + + bool check_neon_session(); + + + csync_instructions_e localRemove(const SyncFileItem &); + csync_instructions_e localMkdir(const SyncFileItem &); + csync_instructions_e remoteRemove(const SyncFileItem &); + csync_instructions_e remoteMkdir(const SyncFileItem &); + csync_instructions_e downloadFile(const SyncFileItem &, bool isConflict = false); + csync_instructions_e uploadFile(const SyncFileItem &); + csync_instructions_e remoteRename(const SyncFileItem &); + + void updateMTimeAndETag(const char *uri, time_t); + + /* fetch the error code and string from the session */ + bool updateErrorFromSession(int neon_code = 0, ne_request *req = NULL); + + + QElapsedTimer _lastTime; + quint64 _lastProgress; + quint64 _chunked_total_size; + quint64 _chunked_done; + QString _currentFile; + + static void notify_status_cb (void *userdata, ne_session_status status, + const ne_session_status_info *info); + +public: + OwncloudPropagator(ne_session_s *session, const QString &localDir, const QString &remoteDir, + ProgressDatabase *progressDb) + : _session(session) + , _localDir(localDir) + , _remoteDir(remoteDir) + , _progressDb(progressDb) + , _errorCode(CSYNC_STATUS_OK) + , _httpStatusCode(0) + , _hasFatalError(false) + { + if (!localDir.endsWith(QChar('/'))) _localDir+='/'; + if (!remoteDir.endsWith(QChar('/'))) _remoteDir+='/'; + } + void propagate(const SyncFileItem &); + QByteArray _etag; + bool _hasFatalError; + + int _downloadLimit; + int _uploadLimit; + +signals: + void completed(const SyncFileItem &, CSYNC_STATUS); + void progress(Progress::Kind, const QString &filename, quint64 bytes, quint64 total); + +}; + +} + +#endif diff --git a/src/mirall/progressdatabase.cpp b/src/mirall/progressdatabase.cpp new file mode 100644 index 000000000..4d6e3ac33 --- /dev/null +++ b/src/mirall/progressdatabase.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) by Olivier Goffart + * + * 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. + */ + +#include "progressdatabase.h" +#include + +namespace Mirall { + +QDataStream& operator<<(QDataStream&d , const ProgressDatabase::DownloadInfo &i) +{ return d << i.tmpfile << i.etag; } +QDataStream& operator>>(QDataStream&d , ProgressDatabase::DownloadInfo &i) +{ return d >> i.tmpfile >> i.etag; } +QDataStream& operator<<(QDataStream&d , const ProgressDatabase::UploadInfo &i) +{ return d << i.chunk << i.transferid << i.size << qlonglong(i.mtime); } +QDataStream& operator>>(QDataStream&d , ProgressDatabase::UploadInfo &i) { + qlonglong mtime; + d >> i.chunk >> i.transferid >> i.size >> mtime; + i.mtime = mtime; + return d; +} + + +void ProgressDatabase::load(const QString& rootDirectory) +{ + QMutexLocker locker(&_mutex); + QFile f(rootDirectory + "/.csync-progressdatabase"); + if (!f.open(QIODevice::ReadOnly)) { + return; + } + QDataStream stream(&f); + + QByteArray magic; + stream >> magic; + if (magic != "csyncpdb_1") + return; + + stream >> _down >> _up; +} + +void ProgressDatabase::save(const QString& rootDirectory) +{ + QMutexLocker locker(&_mutex); + QFile f(rootDirectory + "/.csync-progressdatabase"); + if (!f.open(QIODevice::WriteOnly)) { + return; + } + QDataStream stream(&f); + stream << QByteArray("csyncpdb_1") << _down << _up; +} + +} diff --git a/src/mirall/progressdatabase.h b/src/mirall/progressdatabase.h new file mode 100644 index 000000000..4b4379ac0 --- /dev/null +++ b/src/mirall/progressdatabase.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) by Olivier Goffart + * + * 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. + */ + +#ifndef PROGRESSDATABASE_H +#define PROGRESSDATABASE_H + +#include +#include +#include +#include + + +namespace Mirall { + +class ProgressDatabase { +public: + struct DownloadInfo { + QString tmpfile; + QByteArray etag; + }; + typedef QHash DownloadInfoHash; + struct UploadInfo { + int chunk; + int transferid; + quint64 size; //currently unused + time_t mtime; + }; + typedef QHash UploadInfoHash; + + void load(const QString &rootDirectory); + void save(const QString &rootDirectory); + + const DownloadInfo *getDownloadInfo(const QString &file) const { + QMutexLocker locker(&_mutex); + DownloadInfoHash::const_iterator it = _down.constFind(file); + if (it == _down.end()) + return 0; + return &it.value(); + } + + const UploadInfo *getUploadInfo(const QString &file) const { + QMutexLocker locker(&_mutex); + UploadInfoHash::const_iterator it = _up.constFind(file); + if (it == _up.end()) + return 0; + return &it.value(); + } + + void setDownloadInfo(const QString &file, const DownloadInfo &i) { + QMutexLocker locker(&_mutex); + _down[file] = i; + } + + void setUploadInfo(const QString &file, const UploadInfo &i) { + QMutexLocker locker(&_mutex); + _up[file] = i; + } + + void remove(const QString &file) { + QMutexLocker locker(&_mutex); + _down.remove(file); + _up.remove(file); + } + +private: + DownloadInfoHash _down; + UploadInfoHash _up; + QString _rootPath; + mutable QMutex _mutex; +}; + + +} + +#endif diff --git a/src/mirall/progressdispatcher.h b/src/mirall/progressdispatcher.h index 96a2f1fe5..275424d9d 100644 --- a/src/mirall/progressdispatcher.h +++ b/src/mirall/progressdispatcher.h @@ -27,7 +27,7 @@ namespace Mirall { */ namespace Progress { - typedef enum { + enum Kind { Invalid, StartSync, Download, @@ -42,9 +42,9 @@ namespace Progress StartDelete, EndDelete, Error - } Kind; + }; - typedef struct { + struct Info { Kind kind; QString folder; QString current_file; @@ -58,15 +58,18 @@ namespace Progress QDateTime timestamp; - } Info; + Info() : kind(Invalid), file_size(0), current_file_bytes(0), + overall_file_count(0), current_file_no(0), + overall_transmission_size(0), overall_current_bytes(0) { } + }; - typedef struct { + struct SyncProblem { QString folder; QString current_file; QString error_message; int error_code; QDateTime timestamp; - } SyncProblem; + }; QString asActionString( Kind ); QString asResultString( Kind ); diff --git a/src/mirall/syncfileitem.h b/src/mirall/syncfileitem.h index b6206328c..3885cf3b2 100644 --- a/src/mirall/syncfileitem.h +++ b/src/mirall/syncfileitem.h @@ -2,6 +2,8 @@ #define SYNCFILEITEM_H #include +#include +#include #include @@ -24,10 +26,27 @@ public: SyncFileItem() {} - bool operator==(const SyncFileItem& item) const { - return item._file == this->_file; + friend bool operator==(const SyncFileItem& item1, const SyncFileItem& item2) { + return item1._file == item2._file; } + friend bool operator<(const SyncFileItem& item1, const SyncFileItem& item2) { + // Delete at the end: + if (item1._instruction == CSYNC_INSTRUCTION_REMOVE && item2._instruction != CSYNC_INSTRUCTION_REMOVE) + return false; + if (item1._instruction != CSYNC_INSTRUCTION_REMOVE && item2._instruction == CSYNC_INSTRUCTION_REMOVE) + return true; + + // Sort by destination + return item1.destination() < item2.destination(); + } + + QString destination() const { + return _instruction == CSYNC_INSTRUCTION_RENAME ? _renameTarget : _file; + } + + + bool isEmpty() const { return _file.isEmpty(); } @@ -35,14 +54,27 @@ public: // variables QString _file; QString _renameTarget; - QString _errorString; + QByteArray _originalFile; // as it is in the csync tree csync_instructions_e _instruction; Direction _dir; + bool _isDirectory; + time_t _modtime; + QByteArray _etag; + quint64 _size; + + QString _errorString; + QString _errorDetail; + int _httpCode; + Type _type; }; + + typedef QVector SyncFileItemVector; } +Q_DECLARE_METATYPE(Mirall::SyncFileItem) + #endif // SYNCFILEITEM_H diff --git a/src/owncloudcmd/owncloudcmd.cpp b/src/owncloudcmd/owncloudcmd.cpp new file mode 100644 index 000000000..4c7ecf56c --- /dev/null +++ b/src/owncloudcmd/owncloudcmd.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) by Olivier Goffart + * Copyright (C) by Klaas Freitag + * + * 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. + */ + +#include +#include +#include +#include +#include + +#include "csyncthread.h" +#include "csync.h" + +using namespace Mirall; + +int getauth(const char* prompt, char* buf, size_t len, int echo, int verify, void*) +{ + std::cout << "AUTH CALLBACK\n" << prompt << std::endl; + std::string s; + std::getline(std::cin, s); + strncpy( buf, s.c_str(), len ); + return 0; +} + +void help() { + std::cout << "Usage: owncloudcmd [OPTION...] LOCAL REMOTE\n"; +// " --dry-run This runs only update detection and reconcilation.\n" +// " --proxy= Use an http proxy (ownCloud module only)\n" +} + +struct ProxyInfo { + const char *proxyType; + const char *proxyHost; + int proxyPort; + const char *proxyUser; + const char *proxyPwd; + + ProxyInfo() { + proxyType = 0; + proxyHost = 0; + proxyPort = 0; + proxyUser = 0; + proxyPwd = 0; + } +}; + +int main(int argc, char **argv) { + QCoreApplication app(argc, argv); + + + const char *source_dir = 0; + const char *target_url = 0; + const char *config_directory = 0; + + ProxyInfo proxyInfo; + + for (int i = 1; i < argc; ++i) { + if (argv[i][0] == '-') { + //TODO: parse arguments + std::cerr << "Argument not impemented " << argv[i] << std::endl; + } else if (!source_dir) { + source_dir = argv[i]; + } else if (!target_url) { + target_url = argv[i]; + } else if (!config_directory) { + config_directory = argv[i]; + } else { + std::cerr << "Too many arguments" << std::endl; + return 1; + } + } + + if (!source_dir || !target_url) { + std::cerr << "Too few arguments" << std::endl; + return 1; + } + + + CSYNC *_csync_ctx; + if( csync_create( &_csync_ctx, source_dir, target_url) < 0 ) { + qFatal("Unable to create csync-context!"); + return EXIT_FAILURE; + } + + //csync_set_log_callback( _csync_ctx, csyncLogCatcher ); + csync_set_log_level(11); + csync_enable_conflictcopys(_csync_ctx); + + + csync_set_auth_callback( _csync_ctx, getauth ); + + if( csync_init( _csync_ctx ) < 0 ) { + qFatal("Could not initialize csync!"); + return EXIT_FAILURE; + _csync_ctx = 0; + } + + csync_set_module_property(_csync_ctx, "csync_context", _csync_ctx); + + + CSyncThread csyncthread(_csync_ctx, QString::fromLocal8Bit(source_dir), QUrl(target_url).path()); + QObject::connect(&csyncthread, SIGNAL(finished()), &app, SLOT(quit())); + csyncthread.startSync(); + + app.exec(); + + csync_destroy(_csync_ctx); + return 0; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1a8b5a9be..09aef2295 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,6 @@ include_directories(${CMAKE_CURRENT_LIST_DIR}/../src) +include_directories(${CSYNC_INCLUDE_DIR}/csync ${CSYNC_INCLUDE_DIR} ${CSYNC_INCLUDE_DIR}/httpbf/src ${CSYNC_BUILD_PATH}/src) include(owncloud_add_test.cmake) +owncloud_add_test(OwncloudPropagator) owncloud_add_test(Utility) diff --git a/test/testowncloudpropagator.h b/test/testowncloudpropagator.h new file mode 100644 index 000000000..49840b0c8 --- /dev/null +++ b/test/testowncloudpropagator.h @@ -0,0 +1,28 @@ +/* + This software is in the public domain, furnished "as is", without technical + support, and with no warranty, express or implied, as to its usefulness for + any purpose. +*/ + +#ifndef MIRALL_TESTOWNCLOUDPROPAGATOR_H +#define MIRALL_TESTOWNCLOUDPROPAGATOR_H + +#include + +#include "mirall/owncloudpropagator.h" + +using namespace Mirall; + +class TestOwncloudPropagator : public QObject +{ + Q_OBJECT + +private slots: + void testUpdateErrorFromSession() + { + OwncloudPropagator propagator( NULL, QLatin1String("test1"), QLatin1String("test2")); + QVERIFY( true ); + } +}; + +#endif