Comparar commits
1 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| 79011916d7 |
+1
-1
@@ -27,7 +27,7 @@ https://github.com/owncloud/client.
|
||||
|
||||
## Building the source code
|
||||
|
||||
[Building the Client](http://doc.owncloud.org/desktop/2.2/building.html)
|
||||
[Building the Client](http://doc.owncloud.org/desktop/2.0/building.html)
|
||||
in the ownCloud Desktop Client manual.
|
||||
|
||||
## Maintainers and Contributors
|
||||
|
||||
@@ -9,7 +9,6 @@ StrCpy $PageReinstall_NEW_Field_3 "No instal·lar"
|
||||
StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_TITLE "Ja instal·lat"
|
||||
StrCpy $PageReinstall_NEW_MUI_HEADER_TEXT_SUBTITLE "Trieu la manera com voleu instal·lar ${APPLICATION_NAME}."
|
||||
StrCpy $PageReinstall_OLD_Field_1 "Una versió més recent de ${APPLICATION_NAME} ja està instal.lada!! No es recomana instal.lar una versió més antiga. Si realment voleu instal.lar una versió més antiga, és millor primer desinstal.lar la versió actual. Seleccioni l'operació que desitjeu realitzar i feu clic a Següent per a continuar."
|
||||
StrCpy $PageReinstall_SAME_Field_1 "${APPLICATION_NAME} ${VERSION} ja està instal·lat.$\n$\nSeleccioneu la operació que voleu fer i feu clic a Següent per continuar."
|
||||
StrCpy $PageReinstall_SAME_Field_2 "Afegir/Reinstal.lar components"
|
||||
StrCpy $PageReinstall_SAME_Field_3 "Desinstal.lar ${APPLICATION_NAME}"
|
||||
StrCpy $UNINSTALLER_APPDATA_TITLE "Desinstal.lar ${APPLICATION_NAME}"
|
||||
@@ -38,6 +37,7 @@ StrCpy $UAC_ERROR_ELEVATE "No es pot elevar, error:"
|
||||
StrCpy $UAC_INSTALLER_REQUIRE_ADMIN "Aquest instal·lador requereix accés d'administrador, intenteu-ho de nou"
|
||||
StrCpy $INIT_INSTALLER_RUNNING "L'instal·lador ja s'està executant."
|
||||
StrCpy $UAC_UNINSTALLER_REQUIRE_ADMIN "Aquest desinstal·lador requereix accés d'administrador, intenteu-ho de nou."
|
||||
StrCpy $UAC_ERROR_LOGON_SERVICE "El servei de inici de sessió no s'està executant, s'està abortant!"
|
||||
StrCpy $INIT_UNINSTALLER_RUNNING "El desinstal·lador ja s'està executant."
|
||||
StrCpy $SectionGroup_Shortcuts "Dreceres"
|
||||
StrCpy $PageReinstall_SAME_Field_1 "${APPLICATION_NAME} ${VERSION} is already installed.$\r$\nSelect the operation you want to perform and click Next to continue."
|
||||
StrCpy $UAC_ERROR_LOGON_SERVICE "Logon service is not running, aborting!"
|
||||
|
||||
@@ -32,7 +32,7 @@ find_library(CMOCKA_LIBRARY
|
||||
NAMES
|
||||
cmocka
|
||||
PATHS
|
||||
${CMOCKA_ROOT_DIR}/lib
|
||||
${CMOCKA_ROOT_DIR}/include
|
||||
)
|
||||
|
||||
if (CMOCKA_LIBRARY)
|
||||
|
||||
@@ -34,8 +34,6 @@ if( Qt5Core_FOUND )
|
||||
message(FATAL_ERROR "Qt5WebKit required for Shibboleth. Use -DNO_SHIBBOLETH=1 to disable it.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(Qt5WebSockets)
|
||||
endif()
|
||||
|
||||
else( Qt5Core_FOUND )
|
||||
@@ -69,12 +67,6 @@ if( Qt5Core_FOUND )
|
||||
set (QT_LIBRARIES ${QT_LIBRARIES} ${Qt5MacExtras_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(Qt5WebSockets_FOUND)
|
||||
message(STATUS "Building with websocket notify support")
|
||||
include_directories(${Qt5WebSockets_INCLUDE_DIRS})
|
||||
set (QT_LIBRARIES ${QT_LIBRARIES} ${Qt5WebSockets_LIBRARIES})
|
||||
endif()
|
||||
|
||||
if(NOT BUILD_LIBRARIES_ONLY)
|
||||
macro(qt_wrap_ui)
|
||||
qt5_wrap_ui(${ARGN})
|
||||
|
||||
@@ -65,21 +65,7 @@ static c_rbnode_t *_csync_check_ignored(c_rbtree_t *tree, const char *path, int
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function in the reconcile pass.
|
||||
*
|
||||
* It's called for each entry in the local and remote rbtrees by
|
||||
* csync_reconcile()
|
||||
*
|
||||
* Before the reconcile phase the trees already know about changes
|
||||
* relative to the sync journal. This function's job is to spot conflicts
|
||||
* between local and remote changes and adjust the nodes accordingly.
|
||||
*
|
||||
* See doc/dev/sync-algorithm.md for an overview.
|
||||
*
|
||||
*
|
||||
* Older detail comment:
|
||||
*
|
||||
/*
|
||||
* We merge replicas at the file level. The merged replica contains the
|
||||
* superset of files that are on the local machine and server copies of
|
||||
* the replica. In the case where the same file is in both the local
|
||||
|
||||
@@ -158,19 +158,7 @@ static bool _csync_mtime_equal(time_t a, time_t b)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function of the discovery/update pass.
|
||||
*
|
||||
* It's called (indirectly) by csync_update(), once for each entity in the
|
||||
* local filesystem and once for each entity in the server data.
|
||||
*
|
||||
* It has two main jobs:
|
||||
* - figure out whether anything happened compared to the sync journal
|
||||
* and set (primarily) the instruction flag accordingly
|
||||
* - build the ctx->local.tree / ctx->remote.tree
|
||||
*
|
||||
* See doc/dev/sync-algorithm.md for an overview.
|
||||
*/
|
||||
|
||||
static int _csync_detect_update(CSYNC *ctx, const char *file,
|
||||
const csync_vio_file_stat_t *fs, const int type) {
|
||||
uint64_t h = 0;
|
||||
|
||||
@@ -232,37 +232,6 @@ assertLocalAndRemoteDir( '', 0);
|
||||
system("sqlite3 " . localDir().'.csync_journal.db .dump');
|
||||
|
||||
|
||||
#######################################################################
|
||||
printInfo( "multiple restores of a file create different conflict files" );
|
||||
|
||||
system("sleep 1"); #make sure changes have different mtime
|
||||
|
||||
system("echo 'modified_1' > ". localDir() . "readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN_.data");
|
||||
|
||||
#do the sync
|
||||
csync();
|
||||
assertCsyncJournalOk(localDir());
|
||||
|
||||
system("sleep 1"); #make sure changes have different mtime
|
||||
|
||||
system("echo 'modified_2' > ". localDir() . "readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN_.data");
|
||||
|
||||
#do the sync
|
||||
csync();
|
||||
assertCsyncJournalOk(localDir());
|
||||
|
||||
# there should be two conflict files
|
||||
# TODO check that the conflict file has the right content
|
||||
my @conflicts = glob(localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN__conflict-*.data' );
|
||||
assert( scalar @conflicts == 2 );
|
||||
# remove the conflicts for the next assertLocalAndRemoteDir
|
||||
system("rm " . localDir().'readonlyDirectory_PERM_M_/canotBeModified_PERM_DVN__conflict-*.data' );
|
||||
|
||||
### Both side should still be the same
|
||||
assertLocalAndRemoteDir( '', 0);
|
||||
|
||||
|
||||
|
||||
cleanup();
|
||||
|
||||
|
||||
|
||||
+2
-16
@@ -66,18 +66,11 @@ recipes.
|
||||
|
||||
To set up your build environment for development using HomeBrew_:
|
||||
|
||||
1. Install Xcode
|
||||
2. Install Xcode command line tools::
|
||||
xcode-select --install
|
||||
|
||||
3. Install homebrew::
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
|
||||
4. Add the ownCloud repository using the following command::
|
||||
1. Add the ownCloud repository using the following command::
|
||||
|
||||
brew tap owncloud/owncloud
|
||||
|
||||
5. Install any missing dependencies::
|
||||
2. Install any missing dependencies::
|
||||
|
||||
brew install $(brew deps owncloud-client)
|
||||
|
||||
@@ -87,9 +80,6 @@ To set up your build environment for development using HomeBrew_:
|
||||
|
||||
Where ``x.z`` is the current version of Qt 5 that brew has installed
|
||||
on your machine.
|
||||
4. Install qtkeychain from here: git clone https://github.com/frankosterfeld/qtkeychain.git
|
||||
make sure you make the same install prefix as later while building the client e.g. -
|
||||
``DCMAKE_INSTALL_PREFIX=/Path/to/client-install``
|
||||
|
||||
5. For compilation of the client, follow the :ref:`generic-build-instructions`.
|
||||
|
||||
@@ -244,10 +234,6 @@ To build the most up-to-date version of the client:
|
||||
.. note:: On Mac OS X, you need to specify ``-DCMAKE_INSTALL_PREFIX=target``,
|
||||
where ``target`` is a private location, i.e. in parallel to your build
|
||||
dir by specifying ``../install``.
|
||||
|
||||
..note:: qtkeychain must be compiled with the same prefix e.g CMAKE_INSTALL_PREFIX=/Users/path/to/client/install/
|
||||
|
||||
.. note:: Example:: cmake -DCMAKE_PREFIX_PATH=/usr/local/opt/qt5 -DCMAKE_INSTALL_PREFIX=/Users/path/to/client/install/ -D_OPENSSL_LIBDIR=/usr/local/opt/openssl/lib/ -D_OPENSSL_INCLUDEDIR=/usr/local/opt/openssl/include/ -DOPENSSL_INCLUDE_DIR=/usr/local/opt/openssl/include/ -DNO_SHIBBOLETH=1
|
||||
|
||||
4. Call ``make``.
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
Sync Algorithm
|
||||
==============
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
This is a technical description of the synchronization (sync) algorithm used by the ownCloud client.
|
||||
|
||||
The sync algorithm is the thing that looks at the local and remote file system trees and the sync journal and decides which steps need to be taken to bring the two trees into synchronization. It's different from the propagator, whose job it is to actually execute these steps.
|
||||
|
||||
|
||||
Definitions
|
||||
-----------
|
||||
|
||||
- local tree: The files and directories on the local file system that shall be kept in sync with the remote tree.
|
||||
- remote tree: The files and directories on the ownCloud server that shall be kept in sync with the local tree.
|
||||
- sync journal (journal): A snapshot of file and directory metadata that the sync algorithm uses as a baseline to detect local or remote changes. Typically stored in a database.
|
||||
- file and directory metadata:
|
||||
- mtimes
|
||||
- sizes
|
||||
- inodes (journal and local only): Representation of filesystem object. Useful for rename detection.
|
||||
- etags (journal and remote only): The server assigns a new etag when a file or directory changes.
|
||||
- checksums (journal and remote only): Checksum algorithm applied to a file's contents.
|
||||
- permissions (journal and remote only)
|
||||
|
||||
|
||||
Phases
|
||||
------
|
||||
|
||||
### Discovery (aka Update)
|
||||
|
||||
The discovery phase collects file and directory metadata from the local and remote trees, detecting differences between each tree and the journal.
|
||||
|
||||
Afterwards, we have two trees that tell us what happened relative to the journal. But there may still be conflicts if something happened to an entity both locally and on the remote.
|
||||
|
||||
- Input: file system, server data, journal
|
||||
- Output: two c_rbtree_t*, representing the local and remote trees
|
||||
|
||||
- Note on remote discovery: Since a change to a file on the server causes the etags of all parent folders to change, folders with an unchanged etag can be read from the journal directly and don't need to be walked into.
|
||||
|
||||
- Details
|
||||
- csync_update() uses csync_ftw() on the local and remote trees, one after the other.
|
||||
- csync_ftw() iterates through the entities in a tree and calls csync_walker() for each.
|
||||
- csync_walker() calls _csync_detect_update() on each.
|
||||
- _csync_detect_update() compares the item to its journal entry (if any) to detect new, changed or renamed files. This is the main function of this pass.
|
||||
|
||||
|
||||
|
||||
### Reconcile
|
||||
|
||||
The reconcile phase compares and adjusts the local and remote trees (in both directions), detecting conflicts.
|
||||
|
||||
Afterwards, there are still two trees, but conflicts are marked in them.
|
||||
|
||||
- Input: c_rbtree_t* for the local and remote trees, journal (for some rename-related queries)
|
||||
- Output: changes c_rbtree_t* in-place
|
||||
|
||||
- Details
|
||||
- csync_reconcile() runs csync_reconcile_updates() for the local and remote trees, one after the other.
|
||||
- csync_reconcile_updates() uses c_rbtree_walk() to iterate through the entries, calling _csync_merge_algorithm_visitor() for each.
|
||||
- _csync_merge_algorithm_visitor() checks whether the other tree also has an entry for that node and merges the actions, detecting conflicts. This is the main function of this pass.
|
||||
|
||||
|
||||
### Post-Reconcile
|
||||
|
||||
The post-reconcile phase merges the two trees into one set of SyncFileItems.
|
||||
|
||||
Afterwards, there is a list of items that can tell the propagator what needs to be done.
|
||||
|
||||
- Input: c_rbtree_t* for the local and remote trees
|
||||
- Output: QMap<QString, SyncFileItemPtr>
|
||||
|
||||
- Note that some "propagations", specifically cheap metadata-only updates, are already done at this stage.
|
||||
|
||||
- Details
|
||||
- csync_walk_local_tree() and csync_walk_remote_tree() are called.
|
||||
- They use _csync_walk_tree() to run SyncEngine::treewalkFile() on each entry.
|
||||
- treewalkFile() creates and fills SyncFileItems for each entry, ensuring that each file only has a single instance. This is the main function of this pass.
|
||||
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
||||
An overview of the propagation steps is still missing. The sync protocol is documented at https://github.com/cernbox/smashbox/blob/master/protocol/protocol.md.
|
||||
+4
-83
@@ -415,84 +415,6 @@ X-GNOME-Autostart-Delay=3
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
Comment[oc]=@APPLICATION_NAME@ sincronizacion del client
|
||||
GenericName[oc]=Dorsièr de Sincronizacion
|
||||
@@ -583,10 +505,10 @@ Comment[cs_CZ]=@APPLICATION_NAME@ počítačový synchronizační klient
|
||||
GenericName[cs_CZ]=Synchronizace adresáře
|
||||
Name[cs_CZ]=@APPLICATION_NAME@ počítačový synchronizační klient
|
||||
Icon[cs_CZ]=@APPLICATION_EXECUTABLE@
|
||||
Comment[ru]=Настольный клиент синхронизации @APPLICATION_NAME@
|
||||
GenericName[ru]=Синхронизация каталогов
|
||||
Name[ru]=Настольный клиент синхронизации @APPLICATION_NAME@
|
||||
Icon[ru]=@APPLICATION_EXECUTABLE@
|
||||
Comment[ru]=Настольный клиент синхронизации @НАЗВАНИЕ_ПРИЛОЖЕНИЯ@
|
||||
GenericName[ru]=Синхронизация папки
|
||||
Name[ru]=Настольный клиент синхронизации @НАЗВАНИЕ_ПРИЛОЖЕНИЯ@
|
||||
Icon[ru]=@ВЫПОЛНЯЕМОЕ_ПРИЛОЖЕНИЕ@
|
||||
Comment[sl]=@APPLICATION_NAME@ ‒ Program za usklajevanje datotek z namizjem
|
||||
GenericName[sl]=Usklajevanje map
|
||||
Name[sl]=@APPLICATION_NAME@ ‒ Program za usklajevanje datotek z namizjem
|
||||
@@ -628,7 +550,6 @@ Comment[th_TH]=@APPLICATION_NAME@ ไคลเอนต์ประสานข
|
||||
GenericName[th_TH]=ประสานข้อมูลโฟลเดอร์
|
||||
Name[th_TH]= @APPLICATION_NAME@ ไคลเอนต์ประสานข้อมูลเดสก์ท็อป
|
||||
Icon[th_TH]=@APPLICATION_EXECUTABLE@
|
||||
GenericName[es_MX]=Sincronización de Carpetas
|
||||
Comment[nb_NO]=@APPLICATION_NAME@ skrivebordssynkroniseringsklient
|
||||
GenericName[nb_NO]=Mappesynkronisering
|
||||
Name[nb_NO]=@APPLICATION_NAME@ skrivebordssynkroniseringsklient
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
import os
|
||||
import urllib
|
||||
import socket
|
||||
import tempfile
|
||||
|
||||
from gi.repository import GObject, Nautilus
|
||||
|
||||
@@ -44,7 +43,7 @@ def get_runtime_dir():
|
||||
try:
|
||||
return os.environ['XDG_RUNTIME_DIR']
|
||||
except KeyError:
|
||||
fallback = os.path.join(tempfile.gettempdir(), 'runtime-' + os.environ['USER'])
|
||||
fallback = '/tmp/runtime-' + os.environ['USER']
|
||||
return fallback
|
||||
|
||||
|
||||
@@ -87,17 +86,22 @@ class SocketConnect(GObject.GObject):
|
||||
def _connectToSocketServer(self):
|
||||
try:
|
||||
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock_file = os.path.join(get_runtime_dir(), appname, "socket")
|
||||
try:
|
||||
print("Socket File: " + sock_file)
|
||||
self._sock.connect(sock_file) # fails if sock_file doesn't exist
|
||||
self.connected = True
|
||||
print("Setting connected to %r." % self.connected )
|
||||
self._watch_id = GObject.io_add_watch(self._sock, GObject.IO_IN, self._handle_notify)
|
||||
print("Socket watch id: " + str(self._watch_id))
|
||||
return False # Don't run again
|
||||
except Exception as e:
|
||||
print("Could not connect to unix socket. " + str(e))
|
||||
postfix = "/" + appname + "/socket" # Should use os.path.join instead
|
||||
sock_file = get_runtime_dir() + postfix
|
||||
print ("Socket: " + sock_file + " <=> " + postfix)
|
||||
if sock_file != postfix:
|
||||
try:
|
||||
print("Socket File: " + sock_file)
|
||||
self._sock.connect(sock_file)
|
||||
self.connected = True
|
||||
print("Setting connected to %r." % self.connected )
|
||||
self._watch_id = GObject.io_add_watch(self._sock, GObject.IO_IN, self._handle_notify)
|
||||
print("Socket watch id: " + str(self._watch_id))
|
||||
return False # Don't run again
|
||||
except Exception as e:
|
||||
print("Could not connect to unix socket. " + str(e))
|
||||
else:
|
||||
print("Sock-File not valid: " + sock_file)
|
||||
except Exception as e: # Bad habbit
|
||||
print("Connect could not be established, try again later.")
|
||||
self._sock.close()
|
||||
@@ -153,65 +157,32 @@ class MenuExtension(GObject.GObject, Nautilus.MenuProvider):
|
||||
def __init__(self):
|
||||
GObject.GObject.__init__(self)
|
||||
|
||||
def check_registered_paths(self, filename):
|
||||
topLevelFolder = False
|
||||
internalFile = False
|
||||
for reg_path in socketConnect.registered_paths:
|
||||
if filename == reg_path:
|
||||
topLevelFolder = True
|
||||
break
|
||||
if filename.startswith(reg_path):
|
||||
internalFile = True
|
||||
# you can't have a registered path below another so it is save to break here
|
||||
break
|
||||
return (topLevelFolder, internalFile)
|
||||
|
||||
def get_file_items(self, window, files):
|
||||
# Show the menu extension to share a file or folder
|
||||
#
|
||||
# Show if file is OK.
|
||||
# Ignore top level folders.
|
||||
# Also show extension for folders
|
||||
# if there is a OK or SYNC underneath.
|
||||
# This is only
|
||||
|
||||
if len(files) != 1:
|
||||
return
|
||||
file = files[0]
|
||||
items = []
|
||||
|
||||
filename = get_local_path(file.get_uri())
|
||||
# Check if its a folder (ends with an /), if yes add a "/"
|
||||
# otherwise it will not find the entry in the table
|
||||
isDir = os.path.isdir(filename + os.sep)
|
||||
if isDir:
|
||||
filename += os.sep
|
||||
# Internal or external file?!
|
||||
syncedFile = False
|
||||
for reg_path in socketConnect.registered_paths:
|
||||
topLevelFolder = False
|
||||
filename = get_local_path(file.get_uri())
|
||||
# Check if its a folder (ends with an /), if yes add a "/"
|
||||
# otherwise it will not find the entry in the table
|
||||
if os.path.isdir(filename + "/"):
|
||||
filename += "/"
|
||||
# Check if toplevel folder, we need to ignore those as they cannot be shared
|
||||
if filename == reg_path:
|
||||
topLevelFolder=True
|
||||
# Only show the menu extension if the file is synced and the sync
|
||||
# status is ok. Not for ignored files etc.
|
||||
# ignore top level folders
|
||||
if filename.startswith(reg_path) and topLevelFolder == False and socketConnect.nautilusVFSFile_table[filename]['state'].startswith('OK'):
|
||||
syncedFile = True
|
||||
|
||||
# Check if toplevel folder, we need to ignore those as they cannot be shared
|
||||
topLevelFolder, internalFile = self.check_registered_paths(filename)
|
||||
if topLevelFolder or not internalFile:
|
||||
return items
|
||||
|
||||
entry = socketConnect.nautilusVFSFile_table.get(filename)
|
||||
if not entry:
|
||||
return items
|
||||
|
||||
shareable = False
|
||||
state = entry['state']
|
||||
state_ok = state.startswith('OK')
|
||||
state_sync = state.startswith('SYNC')
|
||||
if state_ok:
|
||||
shareable = True
|
||||
elif state_sync and isDir:
|
||||
# some file below is OK or SYNC
|
||||
for key, value in socketConnect.nautilusVFSFile_table.items():
|
||||
if key != filename and key.startswith(filename):
|
||||
state = value['state']
|
||||
if state.startswith('OK') or state.startswith('SYNC'):
|
||||
shareable = True
|
||||
break
|
||||
|
||||
if not shareable:
|
||||
# If it is neither in a synced folder or is a directory
|
||||
if not syncedFile:
|
||||
return items
|
||||
|
||||
# Create a menu item
|
||||
@@ -332,7 +303,7 @@ class SyncStateExtension(GObject.GObject, Nautilus.ColumnProvider, Nautilus.Info
|
||||
|
||||
filename = get_local_path(item.get_uri())
|
||||
if item.is_directory():
|
||||
filename += os.sep
|
||||
filename += '/'
|
||||
|
||||
inScope = False
|
||||
for reg_path in socketConnect.registered_paths:
|
||||
|
||||
@@ -58,6 +58,7 @@ struct CmdOptions {
|
||||
QString password;
|
||||
QString proxy;
|
||||
bool silent;
|
||||
bool bundleRequests;
|
||||
bool trustSSL;
|
||||
bool useNetrc;
|
||||
bool interactive;
|
||||
@@ -157,6 +158,7 @@ void help()
|
||||
std::cout << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " --silent, -s Don't be so verbose" << std::endl;
|
||||
std::cout << " --bundle-requests, -b Bundle Requests if supported" << std::endl;
|
||||
std::cout << " --httpproxy [proxy] Specify a http proxy to use." << std::endl;
|
||||
std::cout << " Proxy is http://server:port" << std::endl;
|
||||
std::cout << " --trust Trust the SSL certification." << std::endl;
|
||||
@@ -224,6 +226,8 @@ void parseOptions( const QStringList& app_args, CmdOptions *options )
|
||||
options->silent = true;
|
||||
} else if( option == "--trust") {
|
||||
options->trustSSL = true;
|
||||
} else if( option == "-b" || option == "--bundle-requests") {
|
||||
options->bundleRequests = true;
|
||||
} else if( option == "-n") {
|
||||
options->useNetrc = true;
|
||||
} else if( option == "-h") {
|
||||
@@ -292,6 +296,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
CmdOptions options;
|
||||
options.silent = false;
|
||||
options.bundleRequests = false;
|
||||
options.trustSSL = false;
|
||||
options.useNetrc = false;
|
||||
options.interactive = true;
|
||||
@@ -416,6 +421,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
loop.exec();
|
||||
#endif
|
||||
account->setBundleRequestsIfCapable(options.bundleRequests);
|
||||
|
||||
// much lower age than the default since this utility is usually made to be run right after a change in the tests
|
||||
SyncEngine::minimumFileAgeForUpload = 0;
|
||||
|
||||
@@ -199,9 +199,6 @@ IF( WIN32 )
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.rc
|
||||
@ONLY)
|
||||
set(client_version ${CMAKE_CURRENT_BINARY_DIR}/version.rc)
|
||||
IF(NOT MSVC)
|
||||
set(client_manifest ${CMAKE_CURRENT_SOURCE_DIR}/manifest-mingw.rc)
|
||||
ENDIF()
|
||||
ENDIF()
|
||||
|
||||
set( final_src
|
||||
@@ -209,7 +206,6 @@ set( final_src
|
||||
${client_SRCS}
|
||||
${client_UI_SRCS}
|
||||
${client_version}
|
||||
${client_manifest}
|
||||
${guiMoc}
|
||||
${client_I18N}
|
||||
${3rdparty_SRC}
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
#include "filesystem.h"
|
||||
#include "excludedfiles.h"
|
||||
|
||||
// FIXME ifdef
|
||||
#include "json.h"
|
||||
#include <QtWebSockets/QWebSocket>
|
||||
|
||||
#include "creds/abstractcredentials.h"
|
||||
|
||||
#include <QDebug>
|
||||
@@ -120,23 +116,6 @@ Folder::Folder(const FolderDefinition& definition,
|
||||
_scheduleSelfTimer.setInterval(SyncEngine::minimumFileAgeForUpload);
|
||||
connect(&_scheduleSelfTimer, SIGNAL(timeout()),
|
||||
SLOT(slotScheduleThisFolder()));
|
||||
|
||||
_webSocket.reset(new QWebSocket());
|
||||
// FIXME: Also handle re-connect
|
||||
QObject::connect(_webSocket.data(), &QWebSocket::connected, this, &Folder::slotWebSocketConnected);
|
||||
QObject::connect(_webSocket.data(), &QWebSocket::textMessageReceived,
|
||||
this, &Folder::slotWebSocketTextMessageReceived);
|
||||
// FIXME: Accept same ssl errors as normal server?
|
||||
// typedef void (QWebSocket:: *sslErrorsSignal)(const QList<QSslError> &);
|
||||
// connect(&m_webSocket, static_cast<sslErrorsSignal>(&QWebSocket::sslErrors),
|
||||
// this, &SslEchoClient::onSslErrors);
|
||||
// FIXME something with _accountState->account()->davUrl()
|
||||
// FIXME: Check env / capabilities
|
||||
QByteArray wsUrl = qgetenv("OWNCLOUD_WEBSOCKET_URL");
|
||||
if (wsUrl.length() > 0) {
|
||||
qDebug() << "Connecting to websocket " << wsUrl;
|
||||
_webSocket->open(QUrl::fromEncoded(wsUrl));
|
||||
}
|
||||
}
|
||||
|
||||
Folder::~Folder()
|
||||
@@ -321,33 +300,6 @@ void Folder::slotRunEtagJob()
|
||||
// The _requestEtagJob is auto deleting itself on finish. Our guard pointer _requestEtagJob will then be null.
|
||||
}
|
||||
|
||||
void Folder::slotWebSocketConnected()
|
||||
{
|
||||
qDebug() << "CONNECTED";
|
||||
}
|
||||
|
||||
// FIXME: Can we always assume it's a message? the docs are unclear
|
||||
void Folder::slotWebSocketTextMessageReceived(QString message)
|
||||
{
|
||||
qDebug() << message;
|
||||
if (!canSync() || isBusy()) {
|
||||
return;
|
||||
}
|
||||
bool success;
|
||||
QVariantMap json = QtJson::parse(message, success).toMap();
|
||||
if (success) {
|
||||
QString notificationEtag = json["etag"].toString();
|
||||
QString notificationUser = json["user"].toString();
|
||||
QString user = _accountState->account()->credentials()->user();
|
||||
if (user == notificationUser && _lastEtag != notificationEtag) {
|
||||
qDebug() << "* [WebSocket] Compare etag with previous etag: last:" << _lastEtag << ", received:" << notificationEtag << "-> CHANGED";
|
||||
_lastEtag = notificationEtag;
|
||||
slotScheduleThisFolder();
|
||||
_accountState->tagLastSuccessfullETagRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Folder::etagRetreived(const QString& etag)
|
||||
{
|
||||
//qDebug() << "* Compare etag with previous etag: last:" << _lastEtag << ", received:" << etag;
|
||||
|
||||
@@ -27,8 +27,6 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QtWebSockets/QWebSocket>
|
||||
|
||||
|
||||
class QThread;
|
||||
class QSettings;
|
||||
@@ -271,9 +269,6 @@ private slots:
|
||||
void etagRetreived(const QString &);
|
||||
void etagRetreivedFromSyncEngine(const QString &);
|
||||
|
||||
void slotWebSocketConnected();
|
||||
void slotWebSocketTextMessageReceived(QString message);
|
||||
|
||||
void slotThreadTreeWalkResult(const SyncFileItemVector& ); // after sync is done
|
||||
|
||||
void slotEmitFinishedDelayed();
|
||||
@@ -319,8 +314,6 @@ private:
|
||||
bool _wipeDb;
|
||||
bool _proxyDirty;
|
||||
QPointer<RequestEtagJob> _requestEtagJob;
|
||||
// FIXME: ifdef etc
|
||||
QScopedPointer<QWebSocket> _webSocket;
|
||||
QString _lastEtag;
|
||||
QElapsedTimer _timeSinceLastSyncDone;
|
||||
QElapsedTimer _timeSinceLastSyncStart;
|
||||
|
||||
@@ -38,12 +38,6 @@ FolderStatusModel::FolderStatusModel(QObject *parent)
|
||||
FolderStatusModel::~FolderStatusModel()
|
||||
{ }
|
||||
|
||||
static bool sortByFolderHeader(const FolderStatusModel::SubFolderInfo& lhs, const FolderStatusModel::SubFolderInfo& rhs)
|
||||
{
|
||||
return QString::compare(lhs._folder->shortGuiRemotePathOrAppName(),
|
||||
rhs._folder->shortGuiRemotePathOrAppName(),
|
||||
Qt::CaseInsensitive) < 0;
|
||||
}
|
||||
|
||||
void FolderStatusModel::setAccountState(const AccountState* accountState)
|
||||
{
|
||||
@@ -64,6 +58,7 @@ void FolderStatusModel::setAccountState(const AccountState* accountState)
|
||||
if (f->accountState() != accountState)
|
||||
continue;
|
||||
SubFolderInfo info;
|
||||
info._pathIdx << _folders.size();
|
||||
info._name = f->alias();
|
||||
info._path = "/";
|
||||
info._folder = f;
|
||||
@@ -74,14 +69,6 @@ void FolderStatusModel::setAccountState(const AccountState* accountState)
|
||||
connect(f, SIGNAL(newBigFolderDiscovered(QString)), this, SLOT(slotNewBigFolder()), Qt::UniqueConnection);
|
||||
}
|
||||
|
||||
// Sort by header text
|
||||
qSort(_folders.begin(), _folders.end(), sortByFolderHeader);
|
||||
|
||||
// Set the root _pathIdx after the sorting
|
||||
for (int i = 0; i < _folders.size(); ++i) {
|
||||
_folders[i]._pathIdx << i;
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
emit dirtyChanged();
|
||||
}
|
||||
@@ -388,13 +375,12 @@ QModelIndex FolderStatusModel::indexForPath(Folder *f, const QString& path) cons
|
||||
if (slashPos == -1) {
|
||||
// first level folder
|
||||
for (int i = 0; i < _folders.size(); ++i) {
|
||||
auto& info = _folders.at(i);
|
||||
if (info._folder == f) {
|
||||
if (_folders.at(i)._folder == f) {
|
||||
if( path.isEmpty() ) { // the folder object
|
||||
return index(i, 0);
|
||||
}
|
||||
for (int j = 0; j < info._subs.size(); ++j) {
|
||||
const QString subName = info._subs.at(j)._name;
|
||||
for (int j = 0; j < _folders.at(i)._subs.size(); ++j) {
|
||||
const QString subName = _folders.at(i)._subs.at(j)._name;
|
||||
if (subName == path) {
|
||||
return index(j, 0, index(i));
|
||||
}
|
||||
@@ -1000,12 +986,10 @@ void FolderStatusModel::slotFolderSyncStateChange(Folder *f)
|
||||
}
|
||||
if (folderIndex < 0) { return; }
|
||||
|
||||
auto& pi = _folders[folderIndex]._progress;
|
||||
|
||||
SyncResult::Status state = f->syncResult().status();
|
||||
if (f->syncPaused()) {
|
||||
// Reset progress info.
|
||||
pi = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
} else if (state == SyncResult::NotYetStarted) {
|
||||
FolderMan* folderMan = FolderMan::instance();
|
||||
int pos = folderMan->scheduleQueue().indexOf(f);
|
||||
@@ -1019,16 +1003,16 @@ void FolderStatusModel::slotFolderSyncStateChange(Folder *f)
|
||||
} else {
|
||||
message = tr("Waiting for %n other folder(s)...", "", pos);
|
||||
}
|
||||
pi = SubFolderInfo::Progress();
|
||||
pi._overallSyncString = message;
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress._overallSyncString = message;
|
||||
} else if (state == SyncResult::SyncPrepare) {
|
||||
pi = SubFolderInfo::Progress();
|
||||
pi._overallSyncString = tr("Preparing to sync...");
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress._overallSyncString = tr("Preparing to sync...");
|
||||
} else if (state == SyncResult::Problem || state == SyncResult::Success) {
|
||||
// Reset the progress info after a sync.
|
||||
pi = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
} else if (state == SyncResult::Error) {
|
||||
pi = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
}
|
||||
|
||||
// update the icon etc. now
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#include "winuser.h"
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "owncloud.exe.manifest-mingw"
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
|
||||
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level='asInvoker' uiAccess='false' />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
</assembly>
|
||||
@@ -903,9 +903,6 @@ void ownCloudGui::setPauseOnAllFoldersHelper(bool pause)
|
||||
foreach (Folder* f, FolderMan::instance()->map()) {
|
||||
if (accounts.contains(f->accountState())) {
|
||||
f->setSyncPaused(pause);
|
||||
if (pause) {
|
||||
f->slotTerminateSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,11 @@ QNetworkReply *AbstractNetworkJob::davRequest(const QByteArray &verb, const QUrl
|
||||
return addTimer(_account->davRequest(verb, url, req, data));
|
||||
}
|
||||
|
||||
QNetworkReply *AbstractNetworkJob::multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart)
|
||||
{
|
||||
return addTimer(_account->multipartRequest(relPath, req, multiPart));
|
||||
}
|
||||
|
||||
QNetworkReply* AbstractNetworkJob::getRequest(const QString &relPath)
|
||||
{
|
||||
return addTimer(_account->getRequest(relPath));
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "accountfwd.h"
|
||||
|
||||
class QUrl;
|
||||
class QHttpMultiPart;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
@@ -78,6 +79,7 @@ protected:
|
||||
QNetworkReply* headRequest(const QString &relPath);
|
||||
QNetworkReply* headRequest(const QUrl &url);
|
||||
QNetworkReply* deleteRequest(const QUrl &url);
|
||||
QNetworkReply* multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart);
|
||||
|
||||
int maxRedirects() const { return 10; }
|
||||
virtual bool finished() = 0;
|
||||
|
||||
@@ -63,10 +63,6 @@ QNetworkReply* AccessManager::createRequest(QNetworkAccessManager::Operation op,
|
||||
}
|
||||
|
||||
newRequest.setRawHeader(QByteArray("User-Agent"), Utility::userAgentString());
|
||||
|
||||
// Some firewalls reject requests that have a "User-Agent" but no "Accept" header
|
||||
newRequest.setRawHeader(QByteArray("Accept"), "*/*");
|
||||
|
||||
QByteArray verb = newRequest.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
|
||||
// For PROPFIND (assumed to be a WebDAV op), set xml/utf8 as content type/encoding
|
||||
// This needs extension
|
||||
|
||||
@@ -39,6 +39,8 @@ Account::Account(QObject *parent)
|
||||
: QObject(parent)
|
||||
, _capabilities(QVariantMap())
|
||||
, _davPath( Theme::instance()->webDavPath() )
|
||||
, _wasMigrated(false)
|
||||
, _bundleRequests(true)
|
||||
{
|
||||
qRegisterMetaType<AccountPtr>("AccountPtr");
|
||||
}
|
||||
@@ -75,6 +77,13 @@ AccountPtr Account::sharedFromThis()
|
||||
return _sharedThis.toStrongRef();
|
||||
}
|
||||
|
||||
QString Account::davFilesPath() const
|
||||
{
|
||||
//TODO DO NOT HARCODE PATH, GET IT FROM THE SERVER!!!!
|
||||
QString dfp("remote.php/dav/files/");
|
||||
dfp.append(_credentials->user());
|
||||
return dfp;
|
||||
}
|
||||
|
||||
QString Account::displayName() const
|
||||
{
|
||||
@@ -228,6 +237,20 @@ QNetworkReply *Account::davRequest(const QByteArray &verb, const QUrl &url, QNet
|
||||
return _am->sendCustomRequest(req, verb, data);
|
||||
}
|
||||
|
||||
QNetworkReply *Account::multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart)
|
||||
{
|
||||
return multipartRequest(Utility::concatUrlPath(url(), relPath), req, multiPart);
|
||||
}
|
||||
|
||||
QNetworkReply *Account::multipartRequest(const QUrl &url, QNetworkRequest req, QHttpMultiPart *multiPart)
|
||||
{
|
||||
req.setUrl(url);
|
||||
#if QT_VERSION > QT_VERSION_CHECK(4, 8, 4)
|
||||
req.setSslConfiguration(this->getOrCreateSslConfig());
|
||||
#endif
|
||||
return _am->post(req, multiPart);
|
||||
}
|
||||
|
||||
void Account::setCertificate(const QByteArray certficate, const QString privateKey)
|
||||
{
|
||||
_pemCertificate=certficate;
|
||||
@@ -466,6 +489,13 @@ void Account::setNonShib(bool nonShib)
|
||||
}
|
||||
}
|
||||
|
||||
void Account::setBundleRequestsIfCapable(bool bundleRequests){
|
||||
_bundleRequests = bundleRequests;
|
||||
}
|
||||
|
||||
bool Account::bundledRequestsEnabled()
|
||||
{
|
||||
return (_bundleRequests && _capabilities.bundledRequest()) ? true : false;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
+25
-9
@@ -33,6 +33,7 @@ class QSettings;
|
||||
class QNetworkReply;
|
||||
class QUrl;
|
||||
class QNetworkAccessManager;
|
||||
class QHttpMultiPart;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
@@ -63,6 +64,26 @@ public:
|
||||
class OWNCLOUDSYNC_EXPORT Account : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
/**
|
||||
* @brief The possibly themed dav path for the account. It has
|
||||
* a trailing slash.
|
||||
* @returns the (themeable) dav path for the account.
|
||||
*/
|
||||
QString davPath() const;
|
||||
|
||||
/**
|
||||
* @brief The possibly themed files dav path for the account. It has
|
||||
* a trailing slash.
|
||||
* @returns the (themeable) files dav path for the account.
|
||||
*/
|
||||
QString davFilesPath() const;
|
||||
void setDavPath(const QString&s) { _davPath = s; }
|
||||
void setNonShib(bool nonShib);
|
||||
|
||||
/// Functions for bundle support
|
||||
void setBundleRequestsIfCapable(bool bundleRequests);
|
||||
bool bundledRequestsEnabled();
|
||||
|
||||
static AccountPtr create();
|
||||
~Account();
|
||||
|
||||
@@ -78,15 +99,6 @@ public:
|
||||
void setUrl(const QUrl &url);
|
||||
QUrl url() const { return _url; }
|
||||
|
||||
/**
|
||||
* @brief The possibly themed dav path for the account. It has
|
||||
* a trailing slash.
|
||||
* @returns the (themeable) dav path for the account.
|
||||
*/
|
||||
QString davPath() const;
|
||||
void setDavPath(const QString&s) { _davPath = s; }
|
||||
void setNonShib(bool nonShib);
|
||||
|
||||
/** Returns webdav entry URL, based on url() */
|
||||
QUrl davUrl() const;
|
||||
|
||||
@@ -103,6 +115,8 @@ public:
|
||||
QNetworkReply* deleteRequest( const QUrl &url);
|
||||
QNetworkReply* davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data = 0);
|
||||
QNetworkReply* davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data = 0);
|
||||
QNetworkReply* multipartRequest(const QString &relPath, QNetworkRequest req, QHttpMultiPart *multiPart);
|
||||
QNetworkReply* multipartRequest(const QUrl &url, QNetworkRequest req, QHttpMultiPart *multiPart);
|
||||
|
||||
|
||||
/** The ssl configuration during the first connection */
|
||||
@@ -216,6 +230,8 @@ private:
|
||||
QByteArray _pemCertificate;
|
||||
QString _pemPrivateKey;
|
||||
QString _davPath; // defaults to value from theme, might be overwritten in brandings
|
||||
bool _wasMigrated;
|
||||
bool _bundleRequests;
|
||||
friend class AccountManager;
|
||||
};
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ bool Capabilities::sharePublicLink() const
|
||||
}
|
||||
}
|
||||
|
||||
bool Capabilities::bundledRequest() const
|
||||
{
|
||||
bool bundle = _capabilities["dav"].toMap()["bundlerequest"].toBool();
|
||||
return bundle;
|
||||
}
|
||||
|
||||
bool Capabilities::sharePublicLinkAllowUpload() const
|
||||
{
|
||||
return _capabilities["files_sharing"].toMap()["public"].toMap()["upload"].toBool();
|
||||
|
||||
@@ -39,6 +39,7 @@ public:
|
||||
bool sharePublicLinkEnforceExpireDate() const;
|
||||
int sharePublicLinkExpireDateDays() const;
|
||||
bool shareResharing() const;
|
||||
bool bundledRequest() const;
|
||||
|
||||
/// returns true if the capabilities report notifications
|
||||
bool notificationsAvailable() const;
|
||||
|
||||
@@ -303,6 +303,13 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
|
||||
directories.push(qMakePair(QString(), _rootJob.data()));
|
||||
QVector<PropagatorJob*> directoriesToRemove;
|
||||
QString removedDirectory;
|
||||
|
||||
PropagateBundle *bundledUploadRequestsJob = new PropagateBundle(this);
|
||||
quint64 chunkSize = OwncloudPropagator::chunkSize();
|
||||
|
||||
// TODO: here we should also check somehow if bundle is not broken for specific sync - bug recovery to standard PUTs
|
||||
bool bundledRequestsEnabled = _account->bundledRequestsEnabled();
|
||||
|
||||
foreach(const SyncFileItemPtr &item, items) {
|
||||
|
||||
if (!removedDirectory.isEmpty() && item->_file.startsWith(removedDirectory)) {
|
||||
@@ -379,6 +386,12 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
|
||||
currentDirJob->append(dir);
|
||||
}
|
||||
directories.push(qMakePair(item->destination() + "/" , dir));
|
||||
} else if (bundledRequestsEnabled
|
||||
&& (item->_instruction == CSYNC_INSTRUCTION_NEW)
|
||||
&& (item->_direction == SyncFileItem::Up)
|
||||
&& (item->_size < chunkSize)) {
|
||||
//this will create list of bundle files to sync for that bundlejob
|
||||
bundledUploadRequestsJob->append(item);
|
||||
} else if (PropagateItemJob* current = createJob(item)) {
|
||||
if (item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE) {
|
||||
// will delete directories, so defer execution
|
||||
@@ -394,6 +407,21 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
|
||||
_rootJob->append(it);
|
||||
}
|
||||
|
||||
// Root job is DirectoryJob with FullParallelizm, so we need to halt bundle till all other jobs are finished.
|
||||
// If there are no running jobs in root folder it means we can start with sending the bundle
|
||||
// If there are running jobs, we halt and wait for finish of the other job to trigger sending
|
||||
if (bundledRequestsEnabled && !bundledUploadRequestsJob->empty()) {
|
||||
bundledUploadRequestsJob->_item->_direction = SyncFileItem::Direction::Up;
|
||||
bundledUploadRequestsJob->_item->_type = SyncFileItem::Type::RequestsContainer;
|
||||
bundledUploadRequestsJob->_item->_instruction = CSYNC_INSTRUCTION_NEW;
|
||||
bundledUploadRequestsJob->_item->_size = bundledUploadRequestsJob->syncItemsSize();
|
||||
bundledUploadRequestsJob->_item->_originalFile = tr("%1 file(s)").arg(bundledUploadRequestsJob->syncItemsNumber());
|
||||
_rootJob->append(bundledUploadRequestsJob);
|
||||
}
|
||||
else{
|
||||
delete bundledUploadRequestsJob;
|
||||
}
|
||||
|
||||
connect(_rootJob.data(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)),
|
||||
this, SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
|
||||
connect(_rootJob.data(), SIGNAL(progress(const SyncFileItem &,quint64)), this, SIGNAL(progress(const SyncFileItem &,quint64)));
|
||||
@@ -606,15 +634,9 @@ bool PropagateDirectory::scheduleNextJob()
|
||||
return false;
|
||||
}
|
||||
|
||||
// cache the value of first unfinished subjob
|
||||
bool stopAtDirectory = false;
|
||||
int i = _firstUnfinishedSubJob;
|
||||
int subJobsCount = _subJobs.count();
|
||||
while (i < subJobsCount && _subJobs.at(i)->_state == Finished) {
|
||||
_firstUnfinishedSubJob = ++i;
|
||||
}
|
||||
|
||||
for (int i = _firstUnfinishedSubJob; i < subJobsCount; ++i) {
|
||||
// FIXME: use the cached value of finished job
|
||||
for (int i = 0; i < _subJobs.count(); ++i) {
|
||||
if (_subJobs.at(i)->_state == Finished) {
|
||||
continue;
|
||||
}
|
||||
@@ -630,6 +652,7 @@ bool PropagateDirectory::scheduleNextJob()
|
||||
Q_ASSERT(_subJobs.at(i)->_state == Running);
|
||||
|
||||
auto paral = _subJobs.at(i)->parallelism();
|
||||
|
||||
if (paral == WaitForFinished) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -195,11 +195,10 @@ public:
|
||||
int _jobsFinished; // number of jobs that have completed
|
||||
int _runningNow; // number of subJobs running right now
|
||||
SyncFileItem::Status _hasError; // NoStatus, or NormalError / SoftError if there was an error
|
||||
int _firstUnfinishedSubJob;
|
||||
|
||||
explicit PropagateDirectory(OwncloudPropagator *propagator, const SyncFileItemPtr &item = SyncFileItemPtr(new SyncFileItem))
|
||||
: PropagatorJob(propagator)
|
||||
, _firstJob(0), _item(item), _jobsFinished(0), _runningNow(0), _hasError(SyncFileItem::NoStatus), _firstUnfinishedSubJob(0)
|
||||
, _firstJob(0), _item(item), _jobsFinished(0), _runningNow(0), _hasError(SyncFileItem::NoStatus)
|
||||
{ }
|
||||
|
||||
virtual ~PropagateDirectory() {
|
||||
@@ -325,6 +324,10 @@ public:
|
||||
emitFinished(SyncFileItem::NormalError);
|
||||
}
|
||||
|
||||
int runningNowAtRootJob() const {
|
||||
return _rootJob->_runningNow;
|
||||
}
|
||||
|
||||
// timeout in seconds
|
||||
static int httpTimeout();
|
||||
|
||||
@@ -384,6 +387,7 @@ private:
|
||||
friend class PropagateLocalMkdir;
|
||||
friend class PropagateLocalRename;
|
||||
friend class PropagateRemoteMove;
|
||||
friend class PropagateBundle;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -44,10 +44,9 @@ QString ownCloudTheme::configFileName() const
|
||||
QString ownCloudTheme::about() const
|
||||
{
|
||||
QString devString;
|
||||
devString = trUtf8("<p>Version %2. For more information visit <a href=\"%3\">https://%4</a></p>"
|
||||
"<p>For known issues and help, please visit: <a href=\"https://central.owncloud.org/c/help/desktop-file-sync\">https://central.owncloud.org</a></p>"
|
||||
"<p><small>By Klaas Freitag, Daniel Molkentin, Olivier Goffart, Markus Götz, "
|
||||
" Jan-Christoph Borchardt, and others.</small></p>"
|
||||
devString = trUtf8("<p>Version %2. For more information visit <a href=\"%3\">%4</a></p>"
|
||||
"<p><small>By Klaas Freitag, Daniel Molkentin, Jan-Christoph Borchardt, "
|
||||
"Olivier Goffart, Markus Götz and others.</small></p>"
|
||||
"<p>Copyright ownCloud GmbH</p>"
|
||||
"<p>Licensed under the GNU General Public License (GPL) Version 2.0<br/>"
|
||||
"ownCloud and the ownCloud Logo are registered trademarks of ownCloud GmbH "
|
||||
|
||||
@@ -28,11 +28,13 @@ QString Progress::asResultString( const SyncFileItem& item)
|
||||
case CSYNC_INSTRUCTION_SYNC:
|
||||
case CSYNC_INSTRUCTION_NEW:
|
||||
case CSYNC_INSTRUCTION_TYPE_CHANGE:
|
||||
if (item._direction != SyncFileItem::Up) {
|
||||
return QCoreApplication::translate( "progress", "Downloaded");
|
||||
} else {
|
||||
return QCoreApplication::translate( "progress", "Uploaded");
|
||||
}
|
||||
if (item._type != SyncFileItem::Type::RequestsContainer) {
|
||||
if (item._direction != SyncFileItem::Up) {
|
||||
return QCoreApplication::translate( "progress", "Downloaded");
|
||||
} else
|
||||
return QCoreApplication::translate( "progress", "Uploaded");
|
||||
}
|
||||
return QCoreApplication::translate( "progress", "Processed Bundle");
|
||||
case CSYNC_INSTRUCTION_CONFLICT:
|
||||
return QCoreApplication::translate( "progress", "Server version downloaded, copied changed local file into conflict file");
|
||||
case CSYNC_INSTRUCTION_REMOVE:
|
||||
|
||||
@@ -593,7 +593,7 @@ void PropagateDownloadFile::deleteExistingFolder()
|
||||
}
|
||||
|
||||
QString conflictDir = FileSystem::makeConflictFileName(
|
||||
existingDir, Utility::qDateTimeFromTime_t(FileSystem::getModTime(existingDir)));
|
||||
existingDir, Utility::qDateTimeFromTime_t(_item->_modtime));
|
||||
|
||||
emit _propagator->touchedFile(existingDir);
|
||||
emit _propagator->touchedFile(conflictDir);
|
||||
@@ -722,8 +722,7 @@ void PropagateDownloadFile::downloadFinished()
|
||||
&& !FileSystem::fileEquals(fn, _tmpFile.fileName());
|
||||
if (isConflict) {
|
||||
QString renameError;
|
||||
QString conflictFileName = FileSystem::makeConflictFileName(
|
||||
fn, Utility::qDateTimeFromTime_t(FileSystem::getModTime(fn)));
|
||||
QString conflictFileName = FileSystem::makeConflictFileName(fn, Utility::qDateTimeFromTime_t(_item->_modtime));
|
||||
if (!FileSystem::rename(fn, conflictFileName, &renameError)) {
|
||||
// If the rename fails, don't replace it.
|
||||
|
||||
|
||||
@@ -28,10 +28,12 @@
|
||||
|
||||
#include <json.h>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QHttpMultiPart>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <QXmlStreamReader>
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
||||
namespace {
|
||||
@@ -894,5 +896,601 @@ void PropagateUploadFile::abortWithError(SyncFileItem::Status status, const QStr
|
||||
done(status, error);
|
||||
}
|
||||
|
||||
void PropagateBundle::start()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_itemsToChecksum.isEmpty()) {
|
||||
//this will add checksums and remove itself from activeJobList after is completed
|
||||
_preparingBundle = true;
|
||||
_propagator->_activeJobList.append(this);
|
||||
return slotComputeTransmissionChecksum();
|
||||
}
|
||||
|
||||
//this can generate itemDone(), dont add to activeJobList
|
||||
startBundle();
|
||||
}
|
||||
|
||||
void PropagateBundle::slotComputeTransmissionChecksum()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SyncFileItemPtr item = _itemsToChecksum.first();
|
||||
const QString filePath = _propagator->getFilePath(item->_file);
|
||||
|
||||
// remember the modtime before checksumming to be able to detect a file
|
||||
// change during the checksum calculation
|
||||
item->_modtime = FileSystem::getModTime(filePath);
|
||||
|
||||
QByteArray checksumType = contentChecksumType();
|
||||
|
||||
// Maybe the discovery already computed the checksum?
|
||||
if (item->_contentChecksumType == checksumType
|
||||
&& !item->_contentChecksum.isEmpty()) {
|
||||
// Reuse the content checksum as the transmission checksum if possible
|
||||
const auto supportedTransmissionChecksums =
|
||||
_propagator->account()->capabilities().supportedChecksumTypes();
|
||||
if (supportedTransmissionChecksums.contains(checksumType)) {
|
||||
slotStartUpload(checksumType, item->_contentChecksum);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the transmission checksum.
|
||||
auto computeChecksum = new ComputeChecksum(this);
|
||||
if (uploadChecksumEnabled()) {
|
||||
computeChecksum->setChecksumType(_propagator->account()->capabilities().uploadChecksumType());
|
||||
} else {
|
||||
computeChecksum->setChecksumType(QByteArray());
|
||||
}
|
||||
|
||||
connect(computeChecksum, SIGNAL(done(QByteArray,QByteArray)),
|
||||
SLOT(slotStartUpload(QByteArray,QByteArray)));
|
||||
computeChecksum->start(filePath);
|
||||
}
|
||||
|
||||
void PropagateBundle::slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum)
|
||||
{
|
||||
SyncFileItemPtr item = _itemsToChecksum.takeFirst();
|
||||
|
||||
item->_contentChecksum = transmissionChecksum;
|
||||
item->_contentChecksumType = transmissionChecksumType;
|
||||
|
||||
// add this item to sync list, as it now has checksum computed
|
||||
_itemsToSync.append(item);
|
||||
|
||||
_currentBundleSize += item->_size;
|
||||
_currentRequestsNumber++;
|
||||
|
||||
// Remove ourselfs from the list of active job, before any posible call to itemDone()
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
|
||||
// check if we have anything to add to our bundle
|
||||
if (!_itemsToChecksum.empty()){
|
||||
SyncFileItemPtr nextItem = _itemsToChecksum.first();
|
||||
|
||||
// if next item will exceed bundle size, send the bundle now
|
||||
// otherwise, compute next checksum
|
||||
if (((_currentBundleSize + nextItem->_size) >= chunkSize())
|
||||
|| (_currentRequestsNumber >= checkBundledRequestsLimits())){
|
||||
_currentBundleSize = 0;
|
||||
_currentRequestsNumber = 0;
|
||||
startBundle();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
//_itemsToChecksum is already empty, send what is left in _itemsToSync.
|
||||
startBundle();
|
||||
}
|
||||
}
|
||||
|
||||
void PropagateBundle::startBundle()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: use QHttpMultiPartIODevice as as Upload Device
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::MixedType);
|
||||
|
||||
QVector<SyncFileItemPtr> syncItems;
|
||||
while (!_itemsToSync.isEmpty()){
|
||||
SyncFileItemPtr item = _itemsToSync.takeFirst();
|
||||
const QString fullFilePath = _propagator->getFilePath(item->_file);
|
||||
|
||||
if (!FileSystem::fileExists(fullFilePath)) {
|
||||
itemDone(item, SyncFileItem::SoftError, tr("File Removed"));
|
||||
continue;
|
||||
}
|
||||
|
||||
time_t prevModtime = item->_modtime; // the _item value was set in PropagateUploadFileQNAM::start()
|
||||
// but a potential checksum calculation could have taken some time during which the file could
|
||||
// have been changed again, so better check again here.
|
||||
|
||||
item->_modtime = FileSystem::getModTime(fullFilePath);
|
||||
if( prevModtime != item->_modtime ) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
itemDone(item, SyncFileItem::SoftError, tr("Local file changed during syncing. It will be resumed."));
|
||||
continue;
|
||||
}
|
||||
|
||||
quint64 fileSize = FileSystem::getSize(fullFilePath);
|
||||
item->_size = fileSize;
|
||||
|
||||
// But skip the file if the mtime is too close to 'now'!
|
||||
// That usually indicates a file that is still being changed
|
||||
// or not yet fully copied to the destination.
|
||||
if (fileIsStillChanging(*item)) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
itemDone(item, SyncFileItem::SoftError, tr("Local file changed during sync."));
|
||||
continue;
|
||||
}
|
||||
|
||||
//TODO use Upload Device to support bandwith limitation on the client
|
||||
QFile *file = new QFile(fullFilePath);
|
||||
if (! file->open(QIODevice::ReadOnly)) {
|
||||
qDebug() << "ERR: Could not prepare upload device: " << file->errorString();
|
||||
|
||||
// If the file is currently locked, we want to retry the sync
|
||||
// when it becomes available again.
|
||||
if (FileSystem::isFileLocked(fullFilePath)) {
|
||||
emit _propagator->seenLockedFile(fullFilePath);
|
||||
}
|
||||
|
||||
// Soft error because this is likely caused by the user modifying his files while syncing
|
||||
itemDone(item, SyncFileItem::SoftError, tr("ERR: Could not prepare upload device"));
|
||||
} else {
|
||||
QHttpPart bundleContent;
|
||||
bundleContent.setHeader(QNetworkRequest::ContentLengthHeader, QVariant(fileSize));
|
||||
|
||||
//TODO: dont use strings here!! Must change to variable in propagatorjobs.h!
|
||||
bundleContent.setRawHeader("X-OC-Method", QByteArray("PUT"));
|
||||
bundleContent.setRawHeader("X-OC-Path", getRemotePath(item->_file));
|
||||
bundleContent.setRawHeader("X-OC-Mtime", QByteArray::number(qint64(item->_modtime)));
|
||||
|
||||
if (!item->_contentChecksumType.isEmpty()) {
|
||||
bundleContent.setRawHeader(checkSumHeaderC,
|
||||
makeChecksumHeader(item->_contentChecksumType, item->_contentChecksum));
|
||||
}
|
||||
if(item->_file.contains(".sys.admin#recall#")) {
|
||||
// This is a file recall triggered by the admin. Note: the
|
||||
// recall list file created by the admin and downloaded by the
|
||||
// client (.sys.admin#recall#) also falls into this category
|
||||
// (albeit users are not supposed to mess up with it)
|
||||
|
||||
// We use a special tag header so that the server may decide to store this file away in some admin stage area
|
||||
// And not directly in the user's area (which would trigger redownloads etc).
|
||||
bundleContent.setRawHeader("OC-Tag", QByteArray(".sys.admin#recall#"));
|
||||
}
|
||||
|
||||
// File has to be written to multipart body immedietely, because we dont want 1000> open files till the reply comes.
|
||||
bundleContent.setBody(file->readAll());
|
||||
file->close();
|
||||
multiPart->append(bundleContent);
|
||||
syncItems.append(item);
|
||||
}
|
||||
delete file;
|
||||
}
|
||||
|
||||
if (!syncItems.isEmpty())
|
||||
{
|
||||
_propagator->_activeJobList.append(this);
|
||||
|
||||
const QString userPath = _propagator->account()->davFilesPath();
|
||||
|
||||
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
|
||||
MultipartJob* job = new MultipartJob(_propagator->account(), userPath, multiPart, syncItems);
|
||||
job->_duration.start();
|
||||
|
||||
_jobs.append(job);
|
||||
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotMultipartFinished()));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
job->start();
|
||||
_preparingBundle = false;
|
||||
|
||||
// check if there are any items to be bundled and if you can run another parallel job
|
||||
if (!_itemsToChecksum.empty() && (_propagator->_activeJobList.count() < _propagator->maximumActiveJob())) {
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
delete multiPart;
|
||||
}
|
||||
}
|
||||
|
||||
//TODO this function is copied over from owncloudpropagator.cpp (not in owncloudpropagator.h)
|
||||
/** Updates, creates or removes a blacklist entry for the given item.
|
||||
*
|
||||
* Returns whether the file is in the blacklist now.
|
||||
*
|
||||
*/
|
||||
static bool blacklistCheck(SyncJournalDb* journal, const SyncFileItem& item)
|
||||
{
|
||||
SyncJournalErrorBlacklistRecord oldEntry = journal->errorBlacklistEntry(item._file);
|
||||
SyncJournalErrorBlacklistRecord newEntry = SyncJournalErrorBlacklistRecord::update(oldEntry, item);
|
||||
|
||||
if (newEntry.isValid()) {
|
||||
journal->updateErrorBlacklistEntry(newEntry);
|
||||
} else if (oldEntry.isValid()) {
|
||||
journal->wipeErrorBlacklistEntry(item._file);
|
||||
}
|
||||
|
||||
return newEntry.isValid();
|
||||
}
|
||||
|
||||
void PropagateBundle::itemDone(SyncFileItemPtr item, SyncFileItem::Status status, const QString &errorString)
|
||||
{
|
||||
if (item->_isRestoration) {
|
||||
if( status == SyncFileItem::Success || status == SyncFileItem::Conflict) {
|
||||
status = SyncFileItem::Restoration;
|
||||
} else {
|
||||
item->_errorString += tr("; Restoration Failed: %1").arg(errorString);
|
||||
}
|
||||
} else {
|
||||
if( item->_errorString.isEmpty() ) {
|
||||
item->_errorString = errorString;
|
||||
}
|
||||
}
|
||||
|
||||
if( _propagator->_abortRequested.fetchAndAddRelaxed(0) &&
|
||||
(status == SyncFileItem::NormalError || status == SyncFileItem::FatalError)) {
|
||||
// an abort request is ongoing. Change the status to Soft-Error
|
||||
status = SyncFileItem::SoftError;
|
||||
}
|
||||
|
||||
switch( status ) {
|
||||
case SyncFileItem::SoftError:
|
||||
case SyncFileItem::FatalError:
|
||||
// do not blacklist in case of soft error or fatal error.
|
||||
break;
|
||||
case SyncFileItem::NormalError:
|
||||
if (blacklistCheck(_propagator->_journal, *item) && item->_hasBlacklistEntry) {
|
||||
// do not error if the item was, and continues to be, blacklisted
|
||||
status = SyncFileItem::FileIgnored;
|
||||
item->_errorString.prepend(tr("Continue blacklisting:") + " ");
|
||||
}
|
||||
break;
|
||||
case SyncFileItem::Success:
|
||||
case SyncFileItem::Restoration:
|
||||
if( item->_hasBlacklistEntry ) {
|
||||
// wipe blacklist entry.
|
||||
_propagator->_journal->wipeErrorBlacklistEntry(item->_file);
|
||||
// remove a blacklist entry in case the file was moved.
|
||||
if( item->_originalFile != item->_file ) {
|
||||
_propagator->_journal->wipeErrorBlacklistEntry(item->_originalFile);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SyncFileItem::Conflict:
|
||||
case SyncFileItem::FileIgnored:
|
||||
case SyncFileItem::NoStatus:
|
||||
// nothing
|
||||
break;
|
||||
}
|
||||
|
||||
item->_status = status;
|
||||
|
||||
emit itemCompleted(*item, *this);
|
||||
}
|
||||
|
||||
QByteArray PropagateBundle::getRemotePath(QString filePath){
|
||||
QString remotePath(_propagator->_remoteFolder+filePath);
|
||||
return remotePath.toStdString().c_str();
|
||||
}
|
||||
|
||||
void PropagateBundle::append(const SyncFileItemPtr &bundledFile){
|
||||
_size += bundledFile->_size;
|
||||
_itemsToChecksum.append(bundledFile);
|
||||
}
|
||||
|
||||
bool PropagateBundle::empty(){
|
||||
return _itemsToChecksum.empty();
|
||||
}
|
||||
|
||||
void PropagateBundle::slotMultipartFinished()
|
||||
{
|
||||
MultipartJob *job = qobject_cast<MultipartJob *>(sender());
|
||||
Q_ASSERT(job);
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
|
||||
qDebug() << Q_FUNC_INFO << job->reply()->request().url() << "FINISHED WITH STATUS"
|
||||
<< job->reply()->error()
|
||||
<< (job->reply()->error() == QNetworkReply::NoError ? QLatin1String("") : job->reply()->errorString())
|
||||
<< job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute)
|
||||
<< job->reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute);
|
||||
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
||||
if (job->reply()->error() == QNetworkReply::OperationCanceledError && job->reply()->property(owncloudShouldSoftCancelPropertyName).isValid()) {
|
||||
// Abort the job and try again later.
|
||||
// This works around a bug in QNAM wich might reuse a non-empty buffer for the next request.
|
||||
qDebug() << "Forcing job abort on HTTP connection reset with Qt < 5.4.2.";
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
abortWithError(SyncFileItem::SoftError, tr("Forcing job abort on HTTP connection reset with Qt < 5.4.2."));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (err != QNetworkReply::NoError) {
|
||||
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
QByteArray replyContent = job->reply()->readAll();
|
||||
qDebug() << replyContent; // display the XML error in the debug
|
||||
QString errorString = errorMessage(job->errorString(), replyContent);
|
||||
|
||||
if (job->reply()->hasRawHeader("OC-ErrorString")) {
|
||||
errorString = job->reply()->rawHeader("OC-ErrorString");
|
||||
}
|
||||
|
||||
abortWithError(SyncFileItem::SoftError, errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse DAV response
|
||||
QXmlStreamReader reader(job->reply());
|
||||
reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("d", "DAV:"));
|
||||
|
||||
QString currentHref;
|
||||
QString currentOcPath;
|
||||
QString expectedPath(_propagator->account()->davFilesPath());
|
||||
QMap<QString, QString> itemProperties;
|
||||
QMap<QString, QMap<QString, QString> > responseObjectsProperties;
|
||||
bool insidePropstat = false;
|
||||
bool insideProp = false;
|
||||
bool insideError = false;
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader.readNext();
|
||||
QString name = reader.name().toString();
|
||||
// Start elements with DAV:
|
||||
if (type == QXmlStreamReader::StartElement && reader.namespaceUri() == QLatin1String("DAV:")) {
|
||||
if (name == QLatin1String("href")) {
|
||||
// We don't use URL encoding in our request URL (which is the expected path) (QNAM will do it for us)
|
||||
// but the result will have URL encoding..
|
||||
QString hrefString = QString::fromUtf8(QByteArray::fromPercentEncoding(reader.readElementText().toUtf8()));
|
||||
if (!hrefString.endsWith(expectedPath)) {
|
||||
qDebug() << "Invalid href" << hrefString << "expected ending with" << expectedPath;
|
||||
}
|
||||
currentHref = hrefString;
|
||||
} else if (name == QLatin1String("response")) {
|
||||
continue;
|
||||
} else if (name == QLatin1String("propstat")) {
|
||||
insidePropstat = true;
|
||||
} else if (name == QLatin1String("status") && insidePropstat) {
|
||||
QString httpStatus = reader.readElementText();
|
||||
itemProperties.insert(name, httpStatus);
|
||||
} else if (name == QLatin1String("prop")) {
|
||||
insideProp = true;
|
||||
continue;
|
||||
} else if (name == QLatin1String("multistatus")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == QXmlStreamReader::StartElement && insidePropstat && insideProp) {
|
||||
if (name == QLatin1String("oc-path")){
|
||||
currentOcPath = reader.readElementText(QXmlStreamReader::SkipChildElements);
|
||||
} else if (name == QLatin1String("error")){
|
||||
insideError = true;
|
||||
} else if (name == QLatin1String("exception") && insideError){
|
||||
itemProperties.insert(name, reader.readElementText(QXmlStreamReader::SkipChildElements));
|
||||
} else if (name == QLatin1String("message") && insideError){
|
||||
itemProperties.insert(name, reader.readElementText(QXmlStreamReader::SkipChildElements));
|
||||
} else{
|
||||
itemProperties.insert(name, reader.readElementText(QXmlStreamReader::SkipChildElements));
|
||||
}
|
||||
}
|
||||
|
||||
// End elements with DAV:
|
||||
if (type == QXmlStreamReader::EndElement) {
|
||||
if (reader.namespaceUri() == QLatin1String("DAV:")) {
|
||||
if (reader.name() == "response") {
|
||||
currentHref.clear();
|
||||
} else if (reader.name() == "propstat") {
|
||||
insidePropstat = false;
|
||||
if (!currentOcPath.isEmpty()){
|
||||
responseObjectsProperties.insert(currentOcPath, QMap<QString,QString>(itemProperties));
|
||||
}
|
||||
currentOcPath.clear();
|
||||
itemProperties.clear();
|
||||
} else if (reader.name() == "prop") {
|
||||
insideProp = false;
|
||||
} else if (reader.name() == "error") {
|
||||
insideError = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.hasError()) {
|
||||
// XML Parser error? Whatever had been emitted before will come as directoryListingIterated
|
||||
qDebug() << "ERROR" << reader.errorString();
|
||||
abortWithError(SyncFileItem::SoftError, tr("Cannot parse multistatus response!"));
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<SyncFileItemPtr> syncItems = job->syncItems();
|
||||
|
||||
//get the total duration, and try to estimate sync time for single item in the bundle
|
||||
quint64 avgItemDuration = job->_duration.elapsed()/syncItems.count();
|
||||
|
||||
foreach(SyncFileItemPtr item, syncItems) {
|
||||
item->_requestDuration = avgItemDuration;
|
||||
item->_responseTimeStamp = job->responseTimestamp();
|
||||
slotItemFinished(item, responseObjectsProperties);
|
||||
}
|
||||
|
||||
// performance logging
|
||||
qDebug() << "*==* duration BUNDLE UPLOAD" << syncItems.count() << "items synced"
|
||||
<< "average duration" << avgItemDuration;
|
||||
// The job might stay alive for the whole sync, release this tiny bit of memory.
|
||||
|
||||
if (_jobs.empty() && _itemsToChecksum.isEmpty()){
|
||||
done(SyncFileItem::Success);
|
||||
} else if (!_preparingBundle){
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void PropagateBundle::slotItemFinished(const SyncFileItemPtr &item, QMap<QString, QMap<QString, QString> > &responseObjectsProperties)
|
||||
{
|
||||
QString itemFilePath(getRemotePath(item->_file));
|
||||
QMap<QString, QString> fileProperties = responseObjectsProperties.value(itemFilePath);
|
||||
|
||||
item->_httpErrorCode = getHttpStatusCode(fileProperties.value("status"));
|
||||
|
||||
if (200 == item->_httpErrorCode){
|
||||
// Check if the file still exists
|
||||
const QString fullFilePath(_propagator->getFilePath(item->_file));
|
||||
if( !FileSystem::fileExists(fullFilePath) ) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
}
|
||||
|
||||
if (! FileSystem::verifyFileUnchanged(fullFilePath, item->_size, item->_modtime)) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
}
|
||||
|
||||
//OC-FileID section
|
||||
if (fileProperties.contains("oc-fileid")){
|
||||
QString fid = fileProperties.value("oc-fileid");
|
||||
if( !item->_fileId.isEmpty() && item->_fileId != fid ) {
|
||||
qDebug() << "WARN: File ID changed!" << item->_fileId << fid;
|
||||
}
|
||||
item->_fileId =fid.toStdString().c_str();
|
||||
}
|
||||
|
||||
//OC-ETag section
|
||||
QByteArray ocEtag = parseEtag(fileProperties.value("oc-etag").toStdString().c_str());
|
||||
QByteArray etag = parseEtag(fileProperties.value("etag").toStdString().c_str());
|
||||
item->_etag = ocEtag.isEmpty() ? etag : ocEtag;
|
||||
if (ocEtag.length() > 0 && ocEtag != etag) {
|
||||
qDebug() << "Quite peculiar, we have an etag != OC-Etag [no problem!]" << etag << ocEtag;
|
||||
}
|
||||
|
||||
if (fileProperties.value("x-oc-mtime") != "accepted"){
|
||||
// X-OC-MTime is supported since owncloud 5.0. But not when chunking.
|
||||
// Normally Owncloud 6 always puts X-OC-MTime
|
||||
qWarning() << "Server does not support X-OC-MTime" << fileProperties.value("x-oc-mtime");
|
||||
// Well, the mtime was not set
|
||||
itemDone(item, SyncFileItem::SoftError, "Server does not support X-OC-MTime");
|
||||
return;
|
||||
}
|
||||
} else{
|
||||
//We do not check for problems with shared folder since it is only CREATE
|
||||
//TODO: if the file will support update, ensure to override checkForProblemsWithShared()
|
||||
|
||||
qDebug() << Q_FUNC_INFO << item->_file << "ERROR WITH STATUS"
|
||||
<< fileProperties.value("status")
|
||||
<< fileProperties.value("exception")
|
||||
<< fileProperties.value("message");
|
||||
|
||||
if (412 == _item->_httpErrorCode) {
|
||||
// Precondition Failed: Maybe the bad etag is in the database, we need to clear the
|
||||
// parent folder etag so we won't read from DB next sync.
|
||||
_propagator->_journal->avoidReadFromDbOnNextSync(item->_file);
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
}
|
||||
|
||||
item->_errorString = fileProperties.value("message");
|
||||
item->_status = classifyError(QNetworkReply::ContentOperationNotPermittedError, item->_httpErrorCode,
|
||||
&_propagator->_anotherSyncNeeded);
|
||||
itemDone(item, item->_status, item->_errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_propagator->_journal->setFileRecord(SyncJournalFileRecord(*item, _propagator->getFilePath(item->_file)))) {
|
||||
itemDone(item, SyncFileItem::FatalError, tr("Error writing metadata to the database"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from the progress database:
|
||||
_propagator->_journal->setUploadInfo(item->_file, SyncJournalDb::UploadInfo());
|
||||
_propagator->_journal->commit("upload file start");
|
||||
itemDone(item, SyncFileItem::Success);
|
||||
}
|
||||
|
||||
int PropagateBundle::getHttpStatusCode(const QString &status){
|
||||
//if cannot read code, it means that server is not supported and raise 500 Internal Server Error
|
||||
int code = 500;
|
||||
QStringList statusList = status.split(QRegExp("\\s+"));
|
||||
if (2 < statusList.size()){
|
||||
//we expect status list to be at least 3 elements and second element to be the http error code
|
||||
code = statusList.at(1).toInt();
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
void PropagateBundle::slotJobDestroyed(QObject* job)
|
||||
{
|
||||
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job) , _jobs.end());
|
||||
}
|
||||
|
||||
// This function is used whenever there is an error occuring and we need to raise status for whole the bundle
|
||||
void PropagateBundle::abortWithError(SyncFileItem::Status status, const QString &error)
|
||||
{
|
||||
foreach(auto *job, _jobs) {
|
||||
if (job->reply()) {
|
||||
qDebug() << Q_FUNC_INFO << job;
|
||||
job->reply()->abort();
|
||||
}
|
||||
}
|
||||
|
||||
done(status, error);
|
||||
}
|
||||
|
||||
MultipartJob::~MultipartJob()
|
||||
{
|
||||
// Make sure that we destroy the QNetworkReply before our _device of which it keeps an internal pointer.
|
||||
setReply(0);
|
||||
}
|
||||
|
||||
void MultipartJob::start() {
|
||||
QNetworkRequest req;
|
||||
setReply(multipartRequest(path(), req, _multipart));
|
||||
_multipart->setParent(reply()); // delete the multiPart with the job
|
||||
setupConnections(reply());
|
||||
|
||||
if( reply()->error() != QNetworkReply::NoError ) {
|
||||
qWarning() << Q_FUNC_INFO << " Network error: " << reply()->errorString();
|
||||
}
|
||||
|
||||
connect(this, SIGNAL(networkActivity()), account().data(), SIGNAL(propagatorNetworkActivity()));
|
||||
|
||||
//TODO this uses _device, and we have no access here to that
|
||||
// // For Qt versions not including https://codereview.qt-project.org/110150
|
||||
// // Also do the runtime check if compiled with an old Qt but running with fixed one.
|
||||
// // (workaround disabled on windows and mac because the binaries we ship have patched qt)
|
||||
//#if QT_VERSION < QT_VERSION_CHECK(4, 8, 7)
|
||||
// if (QLatin1String(qVersion()) < QLatin1String("4.8.7"))
|
||||
// connect(_device.data(), SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
|
||||
//#elif QT_VERSION > QT_VERSION_CHECK(5, 0, 0) && QT_VERSION < QT_VERSION_CHECK(5, 4, 2) && !defined Q_OS_WIN && !defined Q_OS_MAC
|
||||
// if (QLatin1String(qVersion()) < QLatin1String("5.4.2"))
|
||||
// connect(_device.data(), SIGNAL(wasReset()), this, SLOT(slotSoftAbort()));
|
||||
//#endif
|
||||
|
||||
AbstractNetworkJob::start();
|
||||
}
|
||||
|
||||
void MultipartJob::slotTimeout() {
|
||||
qDebug() << "Timeout" << (reply() ? reply()->request().url() : path());
|
||||
if (!reply())
|
||||
return;
|
||||
_errorString = tr("Connection Timeout");
|
||||
reply()->abort();
|
||||
}
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
||||
void MultipartJob::slotSoftAbort() {
|
||||
reply()->setProperty(owncloudShouldSoftCancelPropertyName, true);
|
||||
reply()->abort();
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include <QFile>
|
||||
#include <QDebug>
|
||||
|
||||
|
||||
namespace OCC {
|
||||
class BandwidthManager;
|
||||
|
||||
@@ -123,6 +122,51 @@ private slots:
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The MultipartJob class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class MultipartJob : public AbstractNetworkJob {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QVector<SyncFileItemPtr> _syncItems;
|
||||
QHttpMultiPart* _multipart;
|
||||
QString _errorString;
|
||||
public:
|
||||
QElapsedTimer _duration;
|
||||
// Takes ownership of the device
|
||||
explicit MultipartJob(AccountPtr account, const QString& path, QHttpMultiPart* multiPart, const QVector<SyncFileItemPtr> &syncItems,QObject* parent = 0)
|
||||
: AbstractNetworkJob(account, path, parent), _syncItems(syncItems), _multipart(multiPart), _errorString(QString()) {}
|
||||
~MultipartJob();
|
||||
|
||||
virtual void start() Q_DECL_OVERRIDE;
|
||||
|
||||
virtual bool finished() Q_DECL_OVERRIDE {
|
||||
emit finishedSignal();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString errorString() {
|
||||
return _errorString.isEmpty() ? reply()->errorString() : _errorString;
|
||||
}
|
||||
|
||||
QVector<SyncFileItemPtr> syncItems() const {
|
||||
return _syncItems;
|
||||
}
|
||||
|
||||
virtual void slotTimeout() Q_DECL_OVERRIDE;
|
||||
|
||||
|
||||
signals:
|
||||
void finishedSignal();
|
||||
|
||||
private slots:
|
||||
#if QT_VERSION < 0x050402
|
||||
void slotSoftAbort();
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief This job implements the asynchronous PUT
|
||||
*
|
||||
@@ -222,5 +266,66 @@ private:
|
||||
void abortWithError(SyncFileItem::Status status, const QString &error);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The PropagateBundle class
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class PropagateBundle : public PropagateItemJob {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QLinkedList<SyncFileItemPtr> _itemsToSync;
|
||||
QLinkedList<SyncFileItemPtr> _itemsToChecksum;
|
||||
QVector<MultipartJob*> _jobs; /// network jobs that are currently in transit
|
||||
bool _running; // Tells that all the jobs have been finished
|
||||
bool _preparingBundle; // Tells that all the jobs have been finished
|
||||
quint64 _size; // Tells what is the total size of _itemsToSync inside the bundle
|
||||
quint64 _currentBundleSize;
|
||||
quint64 _currentRequestsNumber;
|
||||
quint64 _totalFiles;
|
||||
// measure the performance of checksum calc and upload
|
||||
Utility::StopWatch _stopWatch;
|
||||
|
||||
public:
|
||||
PropagateBundle(OwncloudPropagator* propagator)
|
||||
: PropagateItemJob(propagator, SyncFileItemPtr(new SyncFileItem)), _running(false), _preparingBundle(true), _size(0), _currentBundleSize(0), _currentRequestsNumber(0) {}
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
void startBundle();
|
||||
void append(const SyncFileItemPtr &bundledFile);
|
||||
QByteArray getRemotePath(QString filePath);
|
||||
bool empty();
|
||||
bool scheduleNextJob() Q_DECL_OVERRIDE {
|
||||
if (_running != true && _propagator->runningNowAtRootJob() == 1){
|
||||
_running = true;
|
||||
QMetaObject::invokeMethod(this, "start"); // We could be in a different thread (neon jobs)
|
||||
}
|
||||
if (_state == NotYetStarted){
|
||||
_state = Running;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
quint64 syncItemsSize() const { return _size; }
|
||||
quint64 syncItemsNumber() const { return _itemsToChecksum.count(); }
|
||||
private slots:
|
||||
void slotComputeTransmissionChecksum();
|
||||
void slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum);
|
||||
void slotMultipartFinished();
|
||||
void slotJobDestroyed(QObject *job);
|
||||
void abortWithError(SyncFileItem::Status status, const QString &error);
|
||||
|
||||
private:
|
||||
quint64 checkBundledRequestsLimits()
|
||||
{
|
||||
//TODO: obtain this value from the server or by other means
|
||||
quint64 maximumBundledFiles = 500;
|
||||
return (maximumBundledFiles/_propagator->maximumActiveJob());
|
||||
}
|
||||
quint64 chunkSize() const { return _propagator->chunkSize(); }
|
||||
int getHttpStatusCode(const QString &status);
|
||||
void slotItemFinished(const SyncFileItemPtr &item, QMap<QString, QMap<QString, QString> > &responseObjectsProperties);
|
||||
void itemDone(SyncFileItemPtr item, SyncFileItem::Status status, const QString &errorString = QString());
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -334,16 +334,6 @@ int SyncEngine::treewalkRemote( TREE_WALK_FILE* file, void *data )
|
||||
return static_cast<SyncEngine*>(data)->treewalkFile( file, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function in the post-reconcile phase.
|
||||
*
|
||||
* Called on each entry in the local and remote trees by
|
||||
* csync_walk_local_tree()/csync_walk_remote_tree().
|
||||
*
|
||||
* It merges the two csync rbtrees into a single map of SyncFileItems.
|
||||
*
|
||||
* See doc/dev/sync-algorithm.md for an overview.
|
||||
*/
|
||||
int SyncEngine::treewalkFile( TREE_WALK_FILE *file, bool remote )
|
||||
{
|
||||
if( ! file ) return -1;
|
||||
@@ -599,15 +589,11 @@ int SyncEngine::treewalkFile( TREE_WALK_FILE *file, bool remote )
|
||||
// This counts as a NONE for detecting if all the files on the server were changed
|
||||
_hasNoneFiles = true;
|
||||
} else if (!isDirectory) {
|
||||
auto difftime = std::difftime(file->modtime, file->other.modtime);
|
||||
if (difftime < -3600*2) {
|
||||
if (std::difftime(file->modtime, file->other.modtime) < 0) {
|
||||
// We are going back on time
|
||||
// We only increment if the difference is more than two hours to avoid clock skew
|
||||
// issues or DST changes. (We simply ignore files that goes in the past less than
|
||||
// two hours for the backup detection heuristics.)
|
||||
_backInTimeFiles++;
|
||||
qDebug() << file->path << "has a timestamp earlier than the local file";
|
||||
} else if (difftime > 0) {
|
||||
} else {
|
||||
_hasForwardInTimeFiles = true;
|
||||
}
|
||||
}
|
||||
@@ -942,10 +928,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
|
||||
&& _discoveryMainThread->_dataFingerprint != databaseFingerprint) {
|
||||
qDebug() << "data fingerprint changed, assume restore from backup" << databaseFingerprint << _discoveryMainThread->_dataFingerprint;
|
||||
restoreOldFiles();
|
||||
} else if (!_hasForwardInTimeFiles && _backInTimeFiles >= 2 && _account->serverVersionInt() < 0x090100) {
|
||||
// The server before ownCloud 9.1 did not have the data-fingerprint property. So in that
|
||||
// case we use heuristics to detect restored backup. This is disabled with newer version
|
||||
// because this causes troubles to the user and is not as reliable as the data-fingerprint.
|
||||
} else if (!_hasForwardInTimeFiles && _backInTimeFiles >= 2) {
|
||||
qDebug() << "All the changes are bringing files in the past, asking the user";
|
||||
// this typically happen when a backup is restored on the server
|
||||
bool restore = false;
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
|
||||
enum Type {
|
||||
UnknownType = 0,
|
||||
RequestsContainer = 1,
|
||||
File = CSYNC_FTW_TYPE_FILE,
|
||||
Directory = CSYNC_FTW_TYPE_DIR,
|
||||
SoftLink = CSYNC_FTW_TYPE_SLINK
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <QtTest>
|
||||
|
||||
static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
|
||||
static const QUrl sBundleRootUrl("owncloud://somehost/remote.php/dav/files/");
|
||||
|
||||
inline QString generateEtag() {
|
||||
return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
|
||||
@@ -533,6 +534,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class FakeErrorReply : public QNetworkReply
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -556,6 +558,169 @@ public:
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
};
|
||||
|
||||
class FakeBundlePOSTReply : public QNetworkReply
|
||||
{
|
||||
Q_OBJECT
|
||||
FileInfo *fileInfo;
|
||||
QByteArray payload;
|
||||
public:
|
||||
FakeBundlePOSTReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &postPayload, QObject *parent)
|
||||
: QNetworkReply{parent} {
|
||||
setRequest(request);
|
||||
QUrl rawUrl = request.url();
|
||||
QString bundlePath(sBundleRootUrl.path()+rawUrl.userName());
|
||||
setUrl(rawUrl);
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
const QString davUri{QStringLiteral("DAV:")};
|
||||
const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
|
||||
const QString sabUri{QStringLiteral("http://sabredav.org/ns")};
|
||||
QBuffer buffer{&payload};
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
QXmlStreamWriter xml( &buffer );
|
||||
xml.writeNamespace(davUri, "d");
|
||||
xml.writeNamespace(ocUri, "o");
|
||||
xml.writeNamespace(sabUri, "s");
|
||||
|
||||
auto writeFileResponse = [&](const FileInfo &fileInfo) {
|
||||
xml.writeStartElement(davUri, QStringLiteral("response"));
|
||||
|
||||
//TODO: no need for X-OC-PATH, href could contain that, fix client/server
|
||||
xml.writeTextElement(davUri, QStringLiteral("href"), bundlePath);
|
||||
xml.writeStartElement(davUri, QStringLiteral("propstat"));
|
||||
xml.writeStartElement(davUri, QStringLiteral("prop"));
|
||||
|
||||
xml.writeTextElement(davUri, QStringLiteral("oc-etag"), fileInfo.etag);
|
||||
xml.writeTextElement(davUri, QStringLiteral("etag"), fileInfo.etag);
|
||||
xml.writeTextElement(davUri, QStringLiteral("oc-fileid"), fileInfo.fileId);
|
||||
xml.writeTextElement(davUri, QStringLiteral("x-oc-mtime"), QStringLiteral("accepted"));
|
||||
|
||||
//TODO: this slash to be fixed on client/server
|
||||
xml.writeTextElement(davUri, QStringLiteral("oc-path"), "/"+fileInfo.path());
|
||||
xml.writeEndElement(); // prop
|
||||
xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
|
||||
xml.writeEndElement(); // propstat
|
||||
xml.writeEndElement(); // response
|
||||
};
|
||||
auto writeFileErrorResponse = [&](const FileInfo &fileInfo, const QString &exception, const QString &message, const QString &status) {
|
||||
xml.writeStartElement(davUri, QStringLiteral("response"));
|
||||
|
||||
//TODO: no need for X-OC-PATH, href could contain that, fix client/server
|
||||
xml.writeTextElement(davUri, QStringLiteral("href"), bundlePath);
|
||||
xml.writeStartElement(davUri, QStringLiteral("propstat"));
|
||||
xml.writeStartElement(davUri, QStringLiteral("prop"));
|
||||
xml.writeStartElement(davUri, QStringLiteral("error"));
|
||||
xml.writeTextElement(sabUri, QStringLiteral("exception"), exception);
|
||||
xml.writeTextElement(sabUri, QStringLiteral("message"), message);
|
||||
xml.writeEndElement(); // error
|
||||
|
||||
//TODO: this slash to be fixed on client/server
|
||||
xml.writeTextElement(davUri, QStringLiteral("oc-path"), "/"+fileInfo.path());
|
||||
xml.writeEndElement(); // prop
|
||||
xml.writeTextElement(davUri, QStringLiteral("status"), status);
|
||||
xml.writeEndElement(); // propstat
|
||||
xml.writeEndElement(); // response
|
||||
};
|
||||
|
||||
|
||||
if ("erroruser" == rawUrl.userName()) {
|
||||
xml.writeStartDocument();
|
||||
xml.writeStartElement(davUri, QStringLiteral("error"));
|
||||
xml.writeTextElement(sabUri, QStringLiteral("exception"), QStringLiteral("OCA\\DAV\\Connector\\Sabre\\Exception\\Forbidden"));
|
||||
xml.writeTextElement(sabUri, QStringLiteral("message"), QStringLiteral("URL endpoint has to be instance of \\OCA\\DAV\\Files\\FilesHome"));
|
||||
xml.writeTextElement(ocUri, QStringLiteral("retry"), QStringLiteral("false"));
|
||||
xml.writeTextElement(ocUri, QStringLiteral("reason"), QStringLiteral("URL endpoint has to be instance of \\OCA\\DAV\\Files\\FilesHome"));
|
||||
xml.writeEndElement(); // error
|
||||
xml.writeEndDocument();
|
||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 403);
|
||||
} else {
|
||||
Q_ASSERT(request.url().path().endsWith(bundlePath));
|
||||
xml.writeStartDocument();
|
||||
xml.writeStartElement(davUri, QStringLiteral("multistatus"));
|
||||
|
||||
//multipart parsing
|
||||
QString headerSectEnd = "\r\n\r\n";
|
||||
QString headerEnd = "\r\n";
|
||||
QString headerOcMethod = "X-OC-Method: ";
|
||||
QString headerConLen = "Content-Length: ";
|
||||
QString headerOcPath = "X-OC-Path: ";
|
||||
int indexOfBody = 0;
|
||||
QChar contentChar;
|
||||
|
||||
while(postPayload.indexOf(headerSectEnd,indexOfBody) + headerSectEnd.length() >=indexOfBody) {
|
||||
//find oc-method
|
||||
int indexOfheaderEnd = postPayload.indexOf(headerOcMethod,indexOfBody) + headerOcMethod.length();
|
||||
int indexOfheaderBodyEnd = postPayload.indexOf(headerEnd,indexOfheaderEnd);
|
||||
Q_ASSERT(postPayload.mid(indexOfheaderEnd,indexOfheaderBodyEnd-indexOfheaderEnd) == QString("PUT"));
|
||||
|
||||
//find oc-path
|
||||
indexOfheaderEnd = postPayload.indexOf(headerOcPath,indexOfBody) + headerOcPath.length();
|
||||
indexOfheaderBodyEnd = postPayload.indexOf(headerEnd,indexOfheaderEnd)-1;
|
||||
QString filePath(postPayload.mid(indexOfheaderEnd+1,indexOfheaderBodyEnd-indexOfheaderEnd));
|
||||
|
||||
//find content-length
|
||||
indexOfheaderEnd = postPayload.indexOf(headerConLen,indexOfBody) + headerConLen.length();
|
||||
indexOfheaderBodyEnd = postPayload.indexOf(headerEnd,indexOfheaderEnd);
|
||||
QString fileSize(postPayload.mid(indexOfheaderEnd,indexOfheaderBodyEnd-indexOfheaderEnd));
|
||||
|
||||
|
||||
//find body content and extract first letter
|
||||
indexOfheaderEnd = postPayload.indexOf(headerSectEnd,indexOfBody) + headerSectEnd.length();
|
||||
indexOfBody = indexOfheaderEnd+1;
|
||||
contentChar = postPayload.at(indexOfheaderEnd+1);
|
||||
|
||||
if ((fileInfo = remoteRootFileInfo.find(filePath))) {
|
||||
fileInfo->size = fileSize.toInt();
|
||||
fileInfo->contentChar = contentChar.toAscii();
|
||||
} else {
|
||||
// Assume that the file is filled with the same character
|
||||
fileInfo = remoteRootFileInfo.create(filePath, fileSize.toInt(), contentChar.toAscii());
|
||||
}
|
||||
|
||||
if (!fileInfo) {
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
if (filePath.endsWith("normalerrorfile")){
|
||||
writeFileErrorResponse(*fileInfo, QStringLiteral("Sabre\\DAV\\Exception\\BadRequest"), QStringLiteral("Method not allowed - file exists - update of the file is not supported!"), QStringLiteral("HTTP/1.1 400 Bad Request"));
|
||||
} else if (filePath.endsWith("fatalerrorfile")){
|
||||
writeFileErrorResponse(*fileInfo, QStringLiteral("Sabre\\DAV\\Exception\\ServiceUnavailable"), QStringLiteral("Failed to check file size"), QStringLiteral("HTTP/1.1 503 Service Unavailable"));
|
||||
} else if (filePath.endsWith("softerrorfile")){
|
||||
writeFileErrorResponse(*fileInfo, QStringLiteral("OCA\\DAV\\Connector\\Sabre\\Exception\\FileLocked"), QStringLiteral("Target file is locked by another process."), QStringLiteral("HTTP/1.1 423 Locked (WebDAV; RFC 4918)"));
|
||||
} else {
|
||||
writeFileResponse(*fileInfo);
|
||||
}
|
||||
}
|
||||
xml.writeEndElement(); // multistatus
|
||||
xml.writeEndDocument();
|
||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
|
||||
setFinished(true);
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void respond() {
|
||||
setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
|
||||
setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
|
||||
emit metaDataChanged();
|
||||
if (bytesAvailable())
|
||||
emit readyRead();
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void abort() override { }
|
||||
|
||||
qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
|
||||
qint64 readData(char *data, qint64 maxlen) override {
|
||||
qint64 len = std::min(qint64{payload.size()}, maxlen);
|
||||
strncpy(data, payload.constData(), len);
|
||||
payload.remove(0, len);
|
||||
return len;
|
||||
}
|
||||
};
|
||||
|
||||
class FakeQNAM : public QNetworkAccessManager
|
||||
{
|
||||
FileInfo _remoteRootFileInfo;
|
||||
@@ -586,7 +751,9 @@ protected:
|
||||
return new FakeDeleteReply{_remoteRootFileInfo, op, request, this};
|
||||
else if (verb == QLatin1String("MOVE"))
|
||||
return new FakeMoveReply{_remoteRootFileInfo, op, request, this};
|
||||
else {
|
||||
else if (op == QNetworkAccessManager::PostOperation) {
|
||||
return new FakeBundlePOSTReply{_remoteRootFileInfo, op, request, outgoingData->readAll(), this};
|
||||
} else {
|
||||
qDebug() << verb << outgoingData;
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
@@ -703,6 +870,10 @@ public:
|
||||
execUntilFinished();
|
||||
}
|
||||
|
||||
OCC::AccountPtr getAccount() {
|
||||
return _account;
|
||||
}
|
||||
|
||||
private:
|
||||
static void toDisk(QDir &dir, const FileInfo &templateFi) {
|
||||
foreach (const FileInfo &child, templateFi.children) {
|
||||
|
||||
@@ -31,6 +31,16 @@ bool itemDidCompleteSuccessfully(const QSignalSpy &spy, const QString &path)
|
||||
return false;
|
||||
}
|
||||
|
||||
SyncFileItem::Status itemDidCompleteWithStatus(const QSignalSpy &spy, const QString &path)
|
||||
{
|
||||
for(const QList<QVariant> &args : spy) {
|
||||
SyncFileItem item = args[0].value<SyncFileItem>();
|
||||
if (item.destination() == path)
|
||||
return item._status;
|
||||
}
|
||||
return SyncFileItem::NoStatus;
|
||||
}
|
||||
|
||||
class TestSyncEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -54,6 +64,75 @@ private slots:
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
}
|
||||
|
||||
void testFileUploadBundled() {
|
||||
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
|
||||
|
||||
QVariantMap capBundle;
|
||||
capBundle["bundlerequest"] = true;
|
||||
QVariantMap caps;
|
||||
caps["dav"] = capBundle;
|
||||
fakeFolder.getAccount()->setCapabilities(caps);
|
||||
|
||||
testFileUploadBundledAllFilesOK(fakeFolder);
|
||||
testFileUploadBundledErrorForFile(fakeFolder);
|
||||
|
||||
//TODO unfinished, cannot generate NetworkError
|
||||
//testFileUploadBundledNotHomeCollection(fakeFolder);
|
||||
}
|
||||
|
||||
void testFileUploadBundledAllFilesOK(FakeFolder &fakeFolder) {
|
||||
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
|
||||
fakeFolder.localModifier().insert("A/a3");
|
||||
fakeFolder.localModifier().insert("A/a4");
|
||||
fakeFolder.localModifier().insert("B/b0");
|
||||
fakeFolder.syncOnce();
|
||||
|
||||
//check separate files
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a3"));
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a4"));
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "B/b0"));
|
||||
|
||||
//check whole bundle
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, ""));
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
}
|
||||
|
||||
void testFileUploadBundledErrorForFile(FakeFolder &fakeFolder) {
|
||||
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
|
||||
fakeFolder.localModifier().insert("A/a5");
|
||||
fakeFolder.localModifier().insert("A/normalerrorfile");
|
||||
fakeFolder.localModifier().insert("A/fatalerrorfile");
|
||||
fakeFolder.localModifier().insert("A/softerrorfile");
|
||||
fakeFolder.localModifier().insert("B/b3");
|
||||
fakeFolder.syncOnce();
|
||||
|
||||
//check separate files
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "A/a5"));
|
||||
QVERIFY(SyncFileItem::NormalError == itemDidCompleteWithStatus(completeSpy, "A/normalerrorfile"));
|
||||
QVERIFY(SyncFileItem::FatalError == itemDidCompleteWithStatus(completeSpy, "A/fatalerrorfile"));
|
||||
QVERIFY(SyncFileItem::SoftError == itemDidCompleteWithStatus(completeSpy, "A/softerrorfile"));
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, "B/b3"));
|
||||
|
||||
//check whole bundle
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, ""));
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
}
|
||||
|
||||
void testFileUploadBundledNotHomeCollection(FakeFolder &fakeFolder) {
|
||||
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
|
||||
fakeFolder.localModifier().insert("A/a7");
|
||||
fakeFolder.localModifier().insert("A/a8");
|
||||
fakeFolder.localModifier().insert("B/b4");
|
||||
|
||||
//add the user "erroruser" which is not a FilesHomeCollection
|
||||
fakeFolder.getAccount()->setUrl(QUrl(QStringLiteral("http://erroruser:admin@localhost/owncloud")));
|
||||
fakeFolder.syncOnce();
|
||||
|
||||
//check whole bundle
|
||||
QVERIFY(itemDidCompleteSuccessfully(completeSpy, ""));
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
}
|
||||
|
||||
void testDirDownload() {
|
||||
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
|
||||
QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
|
||||
|
||||
@@ -80,7 +80,7 @@ private slots:
|
||||
quint64 sec = 1000;
|
||||
quint64 hour = 3600 * sec;
|
||||
|
||||
QDateTime current = QDateTime::currentDateTimeUtc();
|
||||
QDateTime current = QDateTime::currentDateTime();
|
||||
|
||||
QCOMPARE(durationToDescriptiveString2(0), QString("0 second(s)") );
|
||||
QCOMPARE(durationToDescriptiveString2(5), QString("0 second(s)") );
|
||||
|
||||
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+369
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+369
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+363
-415
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+372
-420
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+363
-415
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+363
-415
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+363
-415
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+372
-418
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+369
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+371
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+371
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+367
-419
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+371
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+373
-419
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+374
-422
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+374
-420
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+366
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+373
-422
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+372
-420
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+380
-428
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+370
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+363
-415
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+370
-417
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+368
-416
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
Referência em uma Nova Issue
Bloquear um usuário