Comparar commits
119 Commits
| Autor | SHA1 | Data | |
|---|---|---|---|
| e6eca69b3e | |||
| 68f99bcc27 | |||
| 7523db354d | |||
| 4919c9e7b1 | |||
| 045498c71a | |||
| 679ac0d26a | |||
| 2e3a3bcf84 | |||
| ad437a49f9 | |||
| 6b2282bf0d | |||
| 42aed56a5a | |||
| ca9ec46253 | |||
| a92eec160a | |||
| cc91c42dfa | |||
| d22d0e7e30 | |||
| 4998303c42 | |||
| ceef2f2d46 | |||
| 8b18600d7e | |||
| 6cc63462b3 | |||
| a1dc4069c9 | |||
| 86846af59d | |||
| 5bfa02602d | |||
| 9be23984eb | |||
| eb8de8e3c0 | |||
| ec7333a4bf | |||
| e485c5c008 | |||
| 6451eb3ade | |||
| 01528427b5 | |||
| cec4d7b9ff | |||
| efa7821dd2 | |||
| 89f55cf9df | |||
| 52552a4204 | |||
| 2f3db04e87 | |||
| 2d6e473a40 | |||
| 92027e8692 | |||
| 2723cd225e | |||
| acd151102c | |||
| 77e790d2ee | |||
| 899c675f0a | |||
| fb76487e76 | |||
| 0c0a3ca0a5 | |||
| 5377d1e283 | |||
| 676ad530e7 | |||
| fbcb01ed27 | |||
| c2b006e857 | |||
| 0e7ccea588 | |||
| c16deb3e44 | |||
| 019544fcfd | |||
| 5f0167b19e | |||
| 29d3c33eb2 | |||
| 469eca1f5f | |||
| 0864d67a9a | |||
| e9503664f5 | |||
| 58cee61624 | |||
| d23d07f99b | |||
| d70db810dd | |||
| 842129f99d | |||
| 36d61ef3a9 | |||
| f1f27221a7 | |||
| eb012d26ee | |||
| 9ee8187083 | |||
| b0c45cfc89 | |||
| 90cea69692 | |||
| 155bdfbffe | |||
| 0e2782d369 | |||
| 6b3d0e69aa | |||
| f30aee6d4e | |||
| 82f86eb019 | |||
| 9573c5e64d | |||
| 15f2b911d9 | |||
| f5aff70398 | |||
| e3a4c3989a | |||
| 3e954bc17f | |||
| 2527569bb8 | |||
| 1e197531cb | |||
| 8b876576eb | |||
| a2287c9657 | |||
| 34c59ba9ed | |||
| c8014a0afd | |||
| 456d82715e | |||
| 8ca3eb7883 | |||
| c110745330 | |||
| 19521863a0 | |||
| c256896165 | |||
| ef922f60fa | |||
| 733ea90e6b | |||
| a4e1fa9dcf | |||
| 1a1684ca2a | |||
| 9c8572e335 | |||
| e675a34fbb | |||
| 5f47c01346 | |||
| 10644d3568 | |||
| 9ee3144358 | |||
| cf48ea2e00 | |||
| 9e2d3f5bc7 | |||
| ffac3f6213 | |||
| 4f1feab845 | |||
| f738cfd7fe | |||
| e6be670e49 | |||
| 46ce2f4722 | |||
| 166a0f60ca | |||
| a9019ccbad | |||
| 0a4806af44 | |||
| e33b89c222 | |||
| 092c935422 | |||
| 273590fdfc | |||
| 0960058842 | |||
| 72d8175032 | |||
| da26e59770 | |||
| 3c24d5a148 | |||
| c222793525 | |||
| 28018e8590 | |||
| 4c79ce2ae6 | |||
| 7c75a39bc1 | |||
| c41f6ed76b | |||
| 818b5854ce | |||
| 79abb8b4e3 | |||
| fad387b6b8 | |||
| a1558100b8 | |||
| 4f3f642da6 |
+1
-1
@@ -27,7 +27,7 @@ https://github.com/owncloud/client.
|
||||
|
||||
## Building the source code
|
||||
|
||||
[Building the Client](http://doc.owncloud.org/desktop/2.0/building.html)
|
||||
[Building the Client](http://doc.owncloud.org/desktop/2.2/building.html)
|
||||
in the ownCloud Desktop Client manual.
|
||||
|
||||
## Maintainers and Contributors
|
||||
|
||||
@@ -9,6 +9,7 @@ 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}"
|
||||
@@ -37,7 +38,6 @@ 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}/include
|
||||
${CMOCKA_ROOT_DIR}/lib
|
||||
)
|
||||
|
||||
if (CMOCKA_LIBRARY)
|
||||
|
||||
+2
-9
@@ -89,7 +89,7 @@ static int _data_cmp(const void *key, const void *data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void csync_create(CSYNC **csync, const char *local, const char *remote) {
|
||||
void csync_create(CSYNC **csync, const char *local) {
|
||||
CSYNC *ctx;
|
||||
size_t len = 0;
|
||||
|
||||
@@ -103,12 +103,6 @@ void csync_create(CSYNC **csync, const char *local, const char *remote) {
|
||||
|
||||
ctx->local.uri = c_strndup(local, len);
|
||||
|
||||
/* remove trailing slashes */
|
||||
len = strlen(remote);
|
||||
while(len > 0 && remote[len - 1] == '/') --len;
|
||||
|
||||
ctx->remote.uri = c_strndup(remote, len);
|
||||
|
||||
ctx->status_code = CSYNC_STATUS_OK;
|
||||
|
||||
ctx->current_fs = NULL;
|
||||
@@ -199,7 +193,7 @@ int csync_update(CSYNC *ctx) {
|
||||
ctx->current = REMOTE_REPLICA;
|
||||
ctx->replica = ctx->remote.type;
|
||||
|
||||
rc = csync_ftw(ctx, ctx->remote.uri, csync_walker, MAX_DEPTH);
|
||||
rc = csync_ftw(ctx, "", csync_walker, MAX_DEPTH);
|
||||
if (rc < 0) {
|
||||
if(ctx->status_code == CSYNC_STATUS_OK) {
|
||||
ctx->status_code = csync_errno_to_status(errno, CSYNC_STATUS_UPDATE_ERROR);
|
||||
@@ -579,7 +573,6 @@ int csync_destroy(CSYNC *ctx) {
|
||||
_csync_clean_ctx(ctx);
|
||||
|
||||
SAFE_FREE(ctx->local.uri);
|
||||
SAFE_FREE(ctx->remote.uri);
|
||||
SAFE_FREE(ctx->error_string);
|
||||
|
||||
#ifdef WITH_ICONV
|
||||
|
||||
+1
-1
@@ -317,7 +317,7 @@ typedef const char* (*csync_checksum_hook) (
|
||||
*
|
||||
* @param csync The context variable to allocate.
|
||||
*/
|
||||
void OCSYNC_EXPORT csync_create(CSYNC **csync, const char *local, const char *remote);
|
||||
void OCSYNC_EXPORT csync_create(CSYNC **csync, const char *local);
|
||||
|
||||
/**
|
||||
* @brief Initialize the file synchronizer.
|
||||
|
||||
@@ -126,7 +126,6 @@ struct csync_s {
|
||||
} local;
|
||||
|
||||
struct {
|
||||
char *uri;
|
||||
c_rbtree_t *tree;
|
||||
enum csync_replica_e type;
|
||||
int read_from_db;
|
||||
|
||||
@@ -65,7 +65,21 @@ 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
|
||||
|
||||
+34
-74
@@ -56,26 +56,13 @@ static uint64_t _hash_of_file(CSYNC *ctx, const char *file) {
|
||||
|
||||
if( ctx && file ) {
|
||||
path = file;
|
||||
switch (ctx->current) {
|
||||
case LOCAL_REPLICA:
|
||||
if (ctx->current == LOCAL_REPLICA) {
|
||||
if (strlen(path) <= strlen(ctx->local.uri)) {
|
||||
return 0;
|
||||
}
|
||||
path += strlen(ctx->local.uri) + 1;
|
||||
break;
|
||||
case REMOTE_REPLICA:
|
||||
if (strlen(path) <= strlen(ctx->remote.uri)) {
|
||||
return 0;
|
||||
}
|
||||
path += strlen(ctx->remote.uri) + 1;
|
||||
break;
|
||||
default:
|
||||
path = NULL;
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
len = strlen(path);
|
||||
|
||||
h = c_jhash64((uint8_t *) path, len, 0);
|
||||
}
|
||||
return h;
|
||||
@@ -158,7 +145,19 @@ 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;
|
||||
@@ -176,25 +175,12 @@ static int _csync_detect_update(CSYNC *ctx, const char *file,
|
||||
}
|
||||
|
||||
path = file;
|
||||
switch (ctx->current) {
|
||||
case LOCAL_REPLICA:
|
||||
if (ctx->current == LOCAL_REPLICA) {
|
||||
if (strlen(path) <= strlen(ctx->local.uri)) {
|
||||
ctx->status_code = CSYNC_STATUS_PARAM_ERROR;
|
||||
return -1;
|
||||
}
|
||||
path += strlen(ctx->local.uri) + 1;
|
||||
break;
|
||||
case REMOTE_REPLICA:
|
||||
if (strlen(path) <= strlen(ctx->remote.uri)) {
|
||||
ctx->status_code = CSYNC_STATUS_PARAM_ERROR;
|
||||
return -1;
|
||||
}
|
||||
path += strlen(ctx->remote.uri) + 1;
|
||||
break;
|
||||
default:
|
||||
path = NULL;
|
||||
ctx->status_code = CSYNC_STATUS_PARAM_ERROR;
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = strlen(path);
|
||||
@@ -617,16 +603,7 @@ int csync_walker(CSYNC *ctx, const char *file, const csync_vio_file_stat_t *fs,
|
||||
|
||||
static bool fill_tree_from_db(CSYNC *ctx, const char *uri)
|
||||
{
|
||||
const char *path = NULL;
|
||||
|
||||
if( strlen(uri) < strlen(ctx->remote.uri)+1) {
|
||||
CSYNC_LOG(CSYNC_LOG_PRIORITY_ERROR, "name does not contain remote uri!");
|
||||
return false;
|
||||
}
|
||||
|
||||
path = uri + strlen(ctx->remote.uri)+1;
|
||||
|
||||
if( csync_statedb_get_below_path(ctx, path) < 0 ) {
|
||||
if( csync_statedb_get_below_path(ctx, uri) < 0 ) {
|
||||
CSYNC_LOG(CSYNC_LOG_PRIORITY_ERROR, "StateDB could not be read!");
|
||||
return false;
|
||||
}
|
||||
@@ -668,12 +645,6 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
|
||||
|
||||
bool do_read_from_db = (ctx->current == REMOTE_REPLICA && ctx->remote.read_from_db);
|
||||
|
||||
if (uri[0] == '\0') {
|
||||
errno = ENOENT;
|
||||
ctx->status_code = CSYNC_STATUS_PARAM_ERROR;
|
||||
goto error;
|
||||
}
|
||||
|
||||
read_from_db = ctx->remote.read_from_db;
|
||||
|
||||
// if the etag of this dir is still the same, its content is restored from the
|
||||
@@ -687,16 +658,7 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
|
||||
goto done;
|
||||
}
|
||||
|
||||
const char *uri_for_vio = uri;
|
||||
if (ctx->current == REMOTE_REPLICA) {
|
||||
uri_for_vio += strlen(ctx->remote.uri);
|
||||
if (strlen(uri_for_vio) > 0 && uri_for_vio[0] == '/') {
|
||||
uri_for_vio++; // cut leading slash
|
||||
}
|
||||
CSYNC_LOG(CSYNC_LOG_PRIORITY_ERROR, "URI without fuzz for %s is \"%s\"", uri, uri_for_vio);
|
||||
}
|
||||
|
||||
if ((dh = csync_vio_opendir(ctx, uri_for_vio)) == NULL) {
|
||||
if ((dh = csync_vio_opendir(ctx, uri)) == NULL) {
|
||||
if (ctx->abort) {
|
||||
CSYNC_LOG(CSYNC_LOG_PRIORITY_TRACE, "Aborted!");
|
||||
ctx->status_code = CSYNC_STATUS_ABORTED;
|
||||
@@ -769,34 +731,32 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
|
||||
continue;
|
||||
}
|
||||
|
||||
flen = asprintf(&filename, "%s/%s", uri, d_name);
|
||||
if (flen < 0) {
|
||||
if (uri[0] == '\0') {
|
||||
filename = c_strdup(d_name);
|
||||
flen = strlen(d_name);
|
||||
} else {
|
||||
flen = asprintf(&filename, "%s/%s", uri, d_name);
|
||||
}
|
||||
if (flen < 0 || !filename) {
|
||||
csync_vio_file_stat_destroy(dirent);
|
||||
dirent = NULL;
|
||||
ctx->status_code = CSYNC_STATUS_MEMORY_ERROR;
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* Create relative path */
|
||||
switch (ctx->current) {
|
||||
case LOCAL_REPLICA:
|
||||
/* Create relative path: For local replica, we need to remove the base path. */
|
||||
path = filename;
|
||||
if (ctx->current == LOCAL_REPLICA) {
|
||||
ulen = strlen(ctx->local.uri) + 1;
|
||||
break;
|
||||
case REMOTE_REPLICA:
|
||||
ulen = strlen(ctx->remote.uri) + 1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (((size_t)flen) < ulen) {
|
||||
csync_vio_file_stat_destroy(dirent);
|
||||
dirent = NULL;
|
||||
ctx->status_code = CSYNC_STATUS_UNSUCCESSFUL;
|
||||
goto error;
|
||||
}
|
||||
path += ulen;
|
||||
}
|
||||
|
||||
if (((size_t)flen) < ulen) {
|
||||
csync_vio_file_stat_destroy(dirent);
|
||||
dirent = NULL;
|
||||
ctx->status_code = CSYNC_STATUS_UNSUCCESSFUL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
path = filename + ulen;
|
||||
|
||||
/* skip ".csync_journal.db" and ".csync_journal.db.ctmp" */
|
||||
/* Isn't this done via csync_exclude already? */
|
||||
|
||||
@@ -30,10 +30,7 @@ static void setup(void **state) {
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
*state = csync;
|
||||
}
|
||||
@@ -45,10 +42,7 @@ static void setup_module(void **state) {
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "dummy://foo/bar");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
csync_init(csync);
|
||||
*state = csync;
|
||||
@@ -66,9 +60,6 @@ static void teardown(void **state) {
|
||||
rc = system("rm -rf /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("rm -rf /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
*state = NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ static void check_csync_create(void **state)
|
||||
|
||||
(void) state; /* unused */
|
||||
|
||||
csync_create(&csync, "/tmp/csync1", "/tmp/csync2");
|
||||
csync_create(&csync, "/tmp/csync1");
|
||||
|
||||
rc = csync_destroy(csync);
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
static void setup(void **state) {
|
||||
CSYNC *csync;
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
*state = csync;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ static void setup_init(void **state) {
|
||||
CSYNC *csync;
|
||||
int rc;
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
rc = csync_exclude_load(EXCLUDE_LIST_FILE, &(csync->excludes));
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
@@ -30,10 +30,7 @@ static void setup(void **state) {
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
*state = csync;
|
||||
}
|
||||
@@ -45,10 +42,7 @@ static void setup_module(void **state) {
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "dummy://foo/bar");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
*state = csync;
|
||||
}
|
||||
@@ -65,9 +59,6 @@ static void teardown(void **state) {
|
||||
rc = system("rm -rf /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("rm -rf /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
*state = NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,7 @@ static void setup(void **state) {
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
*state = csync;
|
||||
}
|
||||
@@ -53,9 +50,6 @@ static void teardown(void **state) {
|
||||
rc = system("rm -rf /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
rc = system("rm -rf /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
*state = NULL;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ static void setup(void **state) {
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
|
||||
csync->statedb.file = c_strdup( TESTDB );
|
||||
*state = csync;
|
||||
|
||||
@@ -34,15 +34,11 @@ static void setup(void **state)
|
||||
|
||||
rc = system("rm -rf /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("rm -rf /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync");
|
||||
assert_int_equal(rc, 0);
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
csync_init(csync);
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
@@ -106,8 +102,6 @@ static void teardown(void **state) {
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("rm -rf /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("rm -rf /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
*state = NULL;
|
||||
}
|
||||
|
||||
@@ -91,9 +91,7 @@ static void setup(void **state)
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
|
||||
csync_create(&csync, "/tmp/check_csync1");
|
||||
csync_init(csync);
|
||||
|
||||
/* Create a new db with metadata */
|
||||
@@ -122,9 +120,7 @@ static void setup_ftw(void **state)
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("mkdir -p /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
csync_create(&csync, "/tmp", "/tmp");
|
||||
csync_create(&csync, "/tmp");
|
||||
csync_init(csync);
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
@@ -162,8 +158,6 @@ static void teardown_rm(void **state) {
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("rm -rf /tmp/check_csync1");
|
||||
assert_int_equal(rc, 0);
|
||||
rc = system("rm -rf /tmp/check_csync2");
|
||||
assert_int_equal(rc, 0);
|
||||
}
|
||||
|
||||
/* create a file stat, caller must free memory */
|
||||
|
||||
@@ -232,6 +232,37 @@ 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();
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ static void setup(void **state)
|
||||
rc = system("rm -rf /tmp/csync_test");
|
||||
assert_int_equal(rc, 0);
|
||||
|
||||
csync_create(&csync, "/tmp/csync1", "/tmp/csync2");
|
||||
csync_create(&csync, "/tmp/csync1");
|
||||
|
||||
csync->replica = LOCAL_REPLICA;
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ static void setup_testenv(void **state) {
|
||||
statevar *mystate = malloc( sizeof(statevar) );
|
||||
mystate->result = NULL;
|
||||
|
||||
csync_create(&(mystate->csync), "/tmp/csync1", "/tmp/csync2");
|
||||
csync_create(&(mystate->csync), "/tmp/csync1");
|
||||
|
||||
mystate->csync->replica = LOCAL_REPLICA;
|
||||
|
||||
|
||||
+29
-3
@@ -66,11 +66,26 @@ recipes.
|
||||
|
||||
To set up your build environment for development using HomeBrew_:
|
||||
|
||||
1. Add the ownCloud repository using the following command::
|
||||
1. Install Xcode
|
||||
2. Install Xcode command line tools::
|
||||
<<<<<<< HEAD
|
||||
xcode-select --install
|
||||
|
||||
3. Install homebrew::
|
||||
=======
|
||||
|
||||
xcode-select --install
|
||||
|
||||
3. Install homebrew::
|
||||
|
||||
>>>>>>> ca9ec4625391ae23940b3a62aaa0afe89f3d98e8
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
|
||||
4. Add the ownCloud repository using the following command::
|
||||
|
||||
brew tap owncloud/owncloud
|
||||
|
||||
2. Install any missing dependencies::
|
||||
5. Install any missing dependencies::
|
||||
|
||||
brew install $(brew deps owncloud-client)
|
||||
|
||||
@@ -80,6 +95,9 @@ 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`.
|
||||
|
||||
@@ -90,7 +108,7 @@ To set up your build environment for development using HomeBrew_:
|
||||
its Common Name as a third parameter (use quotes) to have the package
|
||||
signed automatically.
|
||||
|
||||
.. note:: Contrary to earlier versions, ownCloud 1.7 and later are packaged
|
||||
.. note:: Contrary to earlier versions, ownCloud 1.7 and later are packaged
|
||||
as a ``pkg`` installer. Do not call "make package" at any time when
|
||||
compiling for OS X, as this will build a disk image, and will not
|
||||
work correctly.
|
||||
@@ -234,6 +252,14 @@ 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
|
||||
|
||||
.. 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/ -D_OPENSSL_VERSION=1.0.2a -DOPENSSL_INCLUDE_DIR=/usr/local/opt/openssl/include/ -DNO_SHIBBOLETH=1
|
||||
|
||||
qtkeychain must be compiled with the same prefix e.g CMAKE_INSTALL_PREFIX=/Users/path/to/client/install/ .
|
||||
|
||||
4. Call ``make``.
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
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.
|
||||
@@ -12,7 +12,7 @@ Desktop Sync client enables you to:
|
||||
Your files are always automatically synchronized between your ownCloud server
|
||||
and local PC.
|
||||
|
||||
Because of various technical issues, desktop sync clients older than 1.7 will
|
||||
Because of various technical issues, desktop sync clients older than 2.2.1 will
|
||||
not allowed to connect and sync with the ownCloud 8.1+ server. It is highly
|
||||
recommended to keep your client updated.
|
||||
|
||||
|
||||
+110
-4
@@ -415,6 +415,111 @@ 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
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
# Translations
|
||||
Comment[oc]=@APPLICATION_NAME@ sincronizacion del client
|
||||
GenericName[oc]=Dorsièr de Sincronizacion
|
||||
@@ -505,10 +610,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]=Настольный клиент синхронизации @НАЗВАНИЕ_ПРИЛОЖЕНИЯ@
|
||||
GenericName[ru]=Синхронизация папки
|
||||
Name[ru]=Настольный клиент синхронизации @НАЗВАНИЕ_ПРИЛОЖЕНИЯ@
|
||||
Icon[ru]=@ВЫПОЛНЯЕМОЕ_ПРИЛОЖЕНИЕ@
|
||||
Comment[ru]=Настольный клиент синхронизации @APPLICATION_NAME@
|
||||
GenericName[ru]=Синхронизация каталогов
|
||||
Name[ru]=Настольный клиент синхронизации @APPLICATION_NAME@
|
||||
Icon[ru]=@APPLICATION_EXECUTABLE@
|
||||
Comment[sl]=@APPLICATION_NAME@ ‒ Program za usklajevanje datotek z namizjem
|
||||
GenericName[sl]=Usklajevanje map
|
||||
Name[sl]=@APPLICATION_NAME@ ‒ Program za usklajevanje datotek z namizjem
|
||||
@@ -550,6 +655,7 @@ 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,6 +18,7 @@
|
||||
import os
|
||||
import urllib
|
||||
import socket
|
||||
import tempfile
|
||||
|
||||
from gi.repository import GObject, Nautilus
|
||||
|
||||
@@ -43,7 +44,7 @@ def get_runtime_dir():
|
||||
try:
|
||||
return os.environ['XDG_RUNTIME_DIR']
|
||||
except KeyError:
|
||||
fallback = '/tmp/runtime-' + os.environ['USER']
|
||||
fallback = os.path.join(tempfile.gettempdir(), 'runtime-' + os.environ['USER'])
|
||||
return fallback
|
||||
|
||||
|
||||
@@ -86,22 +87,17 @@ class SocketConnect(GObject.GObject):
|
||||
def _connectToSocketServer(self):
|
||||
try:
|
||||
self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
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)
|
||||
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))
|
||||
except Exception as e: # Bad habbit
|
||||
print("Connect could not be established, try again later.")
|
||||
self._sock.close()
|
||||
@@ -157,32 +153,65 @@ 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 = []
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# If it is neither in a synced folder or is a directory
|
||||
if not syncedFile:
|
||||
# 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:
|
||||
return items
|
||||
|
||||
# Create a menu item
|
||||
@@ -303,7 +332,7 @@ class SyncStateExtension(GObject.GObject, Nautilus.ColumnProvider, Nautilus.Info
|
||||
|
||||
filename = get_local_path(item.get_uri())
|
||||
if item.is_directory():
|
||||
filename += '/'
|
||||
filename += os.sep
|
||||
|
||||
inScope = False
|
||||
for reg_path in socketConnect.registered_paths:
|
||||
|
||||
+2
-11
@@ -329,8 +329,7 @@ int main(int argc, char **argv) {
|
||||
if( !options.target_url.contains( account->davPath() )) {
|
||||
options.target_url.append(account->davPath());
|
||||
}
|
||||
if (options.target_url.startsWith("http"))
|
||||
options.target_url.replace(0, 4, "owncloud");
|
||||
|
||||
QUrl url = QUrl::fromUserInput(options.target_url);
|
||||
|
||||
// Order of retrieval attempt (later attempts override earlier ones):
|
||||
@@ -371,14 +370,6 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
// ### ensure URL is free of credentials
|
||||
if (url.userName().isEmpty()) {
|
||||
url.setUserName(user);
|
||||
}
|
||||
if (url.password().isEmpty()) {
|
||||
url.setPassword(password);
|
||||
}
|
||||
|
||||
// take the unmodified url to pass to csync_create()
|
||||
QByteArray remUrl = options.target_url.toUtf8();
|
||||
|
||||
@@ -475,7 +466,7 @@ restart_sync:
|
||||
selectiveSyncFixup(&db, selectiveSyncList);
|
||||
}
|
||||
|
||||
SyncEngine engine(account, options.source_dir, QUrl(options.target_url), folder, &db);
|
||||
SyncEngine engine(account, options.source_dir, folder, &db);
|
||||
engine.setIgnoreHiddenFiles(options.ignoreHiddenFiles);
|
||||
QObject::connect(&engine, SIGNAL(finished(bool)), &app, SLOT(quit()));
|
||||
QObject::connect(&engine, SIGNAL(transmissionProgress(ProgressInfo)), &cmd, SLOT(transmissionProgressSlot()));
|
||||
|
||||
@@ -199,6 +199,9 @@ 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
|
||||
@@ -206,6 +209,7 @@ set( final_src
|
||||
${client_SRCS}
|
||||
${client_UI_SRCS}
|
||||
${client_version}
|
||||
${client_manifest}
|
||||
${guiMoc}
|
||||
${client_I18N}
|
||||
${3rdparty_SRC}
|
||||
|
||||
@@ -45,7 +45,7 @@ AccountManager *AccountManager::instance()
|
||||
|
||||
bool AccountManager::restore()
|
||||
{
|
||||
auto settings = Account::settingsWithGroup(QLatin1String(accountsC));
|
||||
auto settings = Utility::settingsWithGroup(QLatin1String(accountsC));
|
||||
|
||||
// If there are no accounts, check the old format.
|
||||
if (settings->childGroups().isEmpty()
|
||||
@@ -70,9 +70,7 @@ bool AccountManager::restore()
|
||||
bool AccountManager::restoreFromLegacySettings()
|
||||
{
|
||||
// try to open the correctly themed settings
|
||||
auto settings = Account::settingsWithGroup(Theme::instance()->appName());
|
||||
|
||||
bool migratedCreds = false;
|
||||
auto settings = Utility::settingsWithGroup(Theme::instance()->appName());
|
||||
|
||||
// if the settings file could not be opened, the childKeys list is empty
|
||||
// then try to load settings from a very old place
|
||||
@@ -103,7 +101,6 @@ bool AccountManager::restoreFromLegacySettings()
|
||||
qDebug() << "Migrate oC config if " << oCUrl << " == " << overrideUrl << ":"
|
||||
<< (oCUrl == overrideUrl ? "Yes" : "No");
|
||||
if( oCUrl == overrideUrl ) {
|
||||
migratedCreds = true;
|
||||
settings.reset( oCSettings );
|
||||
} else {
|
||||
delete oCSettings;
|
||||
@@ -115,9 +112,6 @@ bool AccountManager::restoreFromLegacySettings()
|
||||
// Try to load the single account.
|
||||
if (!settings->childKeys().isEmpty()) {
|
||||
if (auto acc = loadAccountHelper(*settings)) {
|
||||
if (migratedCreds) {
|
||||
acc->setMigrated(true);
|
||||
}
|
||||
addAccount(acc);
|
||||
return true;
|
||||
}
|
||||
@@ -127,7 +121,7 @@ bool AccountManager::restoreFromLegacySettings()
|
||||
|
||||
void AccountManager::save(bool saveCredentials)
|
||||
{
|
||||
auto settings = Account::settingsWithGroup(QLatin1String(accountsC));
|
||||
auto settings = Utility::settingsWithGroup(QLatin1String(accountsC));
|
||||
settings->setValue(QLatin1String(versionC), 2);
|
||||
foreach (const auto &acc, _accounts) {
|
||||
settings->beginGroup(acc->account()->id());
|
||||
@@ -143,7 +137,7 @@ void AccountManager::save(bool saveCredentials)
|
||||
void AccountManager::saveAccount(Account* a)
|
||||
{
|
||||
qDebug() << "Saving account" << a->url().toString();
|
||||
auto settings = Account::settingsWithGroup(QLatin1String(accountsC));
|
||||
auto settings = Utility::settingsWithGroup(QLatin1String(accountsC));
|
||||
settings->beginGroup(a->id());
|
||||
saveAccountHelper(a, *settings, false); // don't save credentials they might not have been loaded yet
|
||||
settings->endGroup();
|
||||
@@ -155,7 +149,7 @@ void AccountManager::saveAccount(Account* a)
|
||||
void AccountManager::saveAccountState(AccountState* a)
|
||||
{
|
||||
qDebug() << "Saving account state" << a->account()->url().toString();
|
||||
auto settings = Account::settingsWithGroup(QLatin1String(accountsC));
|
||||
auto settings = Utility::settingsWithGroup(QLatin1String(accountsC));
|
||||
settings->beginGroup(a->account()->id());
|
||||
a->writeToSettings(*settings);
|
||||
settings->endGroup();
|
||||
@@ -281,7 +275,7 @@ void AccountManager::deleteAccount(AccountState* account)
|
||||
auto copy = *it; // keep a reference to the shared pointer so it does not delete it just yet
|
||||
_accounts.erase(it);
|
||||
|
||||
auto settings = Account::settingsWithGroup(QLatin1String(accountsC));
|
||||
auto settings = Utility::settingsWithGroup(QLatin1String(accountsC));
|
||||
settings->remove(account->account()->id());
|
||||
|
||||
emit accountRemoved(account);
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "owncloudsetupwizard.h"
|
||||
#include "creds/abstractcredentials.h"
|
||||
#include "tooltipupdater.h"
|
||||
#include "filesystem.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
@@ -110,7 +111,7 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) :
|
||||
|
||||
QAction *syncNowAction = new QAction(this);
|
||||
syncNowAction->setShortcut(QKeySequence(Qt::Key_F6));
|
||||
connect(syncNowAction, SIGNAL(triggered()), SLOT(slotSyncCurrentFolderNow()));
|
||||
connect(syncNowAction, SIGNAL(triggered()), SLOT(slotScheduleCurrentFolder()));
|
||||
addAction(syncNowAction);
|
||||
|
||||
connect(ui->_folderList, SIGNAL(clicked(const QModelIndex &)),
|
||||
@@ -228,6 +229,11 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos)
|
||||
connect(ac, SIGNAL(triggered(bool)), this, SLOT(doExpand()));
|
||||
}
|
||||
|
||||
if (!folderPaused) {
|
||||
ac = menu->addAction(tr("Force sync now"));
|
||||
connect(ac, SIGNAL(triggered(bool)), this, SLOT(slotForceSyncCurrentFolder()));
|
||||
}
|
||||
|
||||
ac = menu->addAction(folderPaused ? tr("Resume sync") : tr("Pause sync"));
|
||||
connect(ac, SIGNAL(triggered(bool)), this, SLOT(slotEnableCurrentFolder()));
|
||||
|
||||
@@ -300,8 +306,9 @@ void AccountSettings::slotFolderWizardAccepted()
|
||||
tr("<p>Could not create local folder <i>%1</i>.")
|
||||
.arg(QDir::toNativeSeparators(definition.localPath)));
|
||||
return;
|
||||
} else {
|
||||
FileSystem::setFolderMinimumPermissions(definition.localPath);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +468,7 @@ void AccountSettings::slotEnableCurrentFolder()
|
||||
}
|
||||
}
|
||||
|
||||
void AccountSettings::slotSyncCurrentFolderNow()
|
||||
void AccountSettings::slotScheduleCurrentFolder()
|
||||
{
|
||||
QModelIndex selected = ui->_folderList->selectionModel()->currentIndex();
|
||||
if( !selected.isValid() )
|
||||
@@ -472,6 +479,24 @@ void AccountSettings::slotSyncCurrentFolderNow()
|
||||
folderMan->scheduleFolder(folderMan->folder(alias));
|
||||
}
|
||||
|
||||
void AccountSettings::slotForceSyncCurrentFolder()
|
||||
{
|
||||
QModelIndex selected = ui->_folderList->selectionModel()->currentIndex();
|
||||
if( !selected.isValid() )
|
||||
return;
|
||||
QString alias = _model->data( selected, FolderStatusDelegate::FolderAliasRole ).toString();
|
||||
FolderMan *folderMan = FolderMan::instance();
|
||||
|
||||
// Terminate and reschedule any running sync
|
||||
if (Folder* current = folderMan->currentSyncFolder()) {
|
||||
folderMan->terminateSyncProcess();
|
||||
folderMan->scheduleFolder(current);
|
||||
}
|
||||
|
||||
// Insert the selected folder at the front of the queue
|
||||
folderMan->scheduleFolderNext(folderMan->folder(alias));
|
||||
}
|
||||
|
||||
void AccountSettings::slotOpenOC()
|
||||
{
|
||||
if( _OCUrl.isValid() )
|
||||
|
||||
@@ -71,7 +71,8 @@ public slots:
|
||||
protected slots:
|
||||
void slotAddFolder();
|
||||
void slotEnableCurrentFolder();
|
||||
void slotSyncCurrentFolderNow();
|
||||
void slotScheduleCurrentFolder();
|
||||
void slotForceSyncCurrentFolder();
|
||||
void slotRemoveCurrentFolder();
|
||||
void slotOpenCurrentFolder();
|
||||
void slotFolderWizardAccepted();
|
||||
|
||||
@@ -307,7 +307,7 @@ void AccountState::slotCredentialsAsked(AbstractCredentials* credentials)
|
||||
|
||||
std::unique_ptr<QSettings> AccountState::settings()
|
||||
{
|
||||
auto s = _account->settingsWithGroup(QLatin1String("Accounts"));
|
||||
auto s = Utility::settingsWithGroup(QLatin1String("Accounts"));
|
||||
s->beginGroup(_account->id());
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -617,7 +617,7 @@ void ActivitySettings::slotCopyToClipboard()
|
||||
message = tr("The sync activity list has been copied to the clipboard.");
|
||||
} else if(idx == 2 ) {
|
||||
// issues Widget
|
||||
message = tr("The list of unsynched items has been copied to the clipboard.");
|
||||
message = tr("The list of unsynced items has been copied to the clipboard.");
|
||||
_protocolWidget->storeSyncIssues(ts);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,21 +80,6 @@ void ShibbolethCredentials::setAccount(Account* account)
|
||||
}
|
||||
}
|
||||
|
||||
bool ShibbolethCredentials::changed(AbstractCredentials* credentials) const
|
||||
{
|
||||
ShibbolethCredentials* other(qobject_cast< ShibbolethCredentials* >(credentials));
|
||||
|
||||
if (!other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_shibCookie != other->_shibCookie || _user != other->_user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString ShibbolethCredentials::authType() const
|
||||
{
|
||||
return QString::fromLatin1("shibboleth");
|
||||
@@ -143,7 +128,7 @@ void ShibbolethCredentials::fetchFromKeychain()
|
||||
} else {
|
||||
_url = _account->url();
|
||||
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
||||
job->setSettings(_account->settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
job->setSettings(Utility::settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
job->setInsecureFallback(false);
|
||||
job->setKey(keychainKey(_account->url().toString(), "shibAssertion"));
|
||||
connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotReadJobDone(QKeychain::Job*)));
|
||||
@@ -262,7 +247,7 @@ void ShibbolethCredentials::slotReadJobDone(QKeychain::Job *job)
|
||||
addToCookieJar(_shibCookie);
|
||||
}
|
||||
// access
|
||||
job->setSettings(_account->settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
job->setSettings(Utility::settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
|
||||
_ready = true;
|
||||
_stillValid = true;
|
||||
@@ -321,7 +306,7 @@ QByteArray ShibbolethCredentials::shibCookieName()
|
||||
void ShibbolethCredentials::storeShibCookie(const QNetworkCookie &cookie)
|
||||
{
|
||||
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
||||
job->setSettings(_account->settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
job->setSettings(Utility::settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
// we don't really care if it works...
|
||||
//connect(job, SIGNAL(finished(QKeychain::Job*)), SLOT(slotWriteJobDone(QKeychain::Job*)));
|
||||
job->setKey(keychainKey(_account->url().toString(), "shibAssertion"));
|
||||
@@ -332,7 +317,7 @@ void ShibbolethCredentials::storeShibCookie(const QNetworkCookie &cookie)
|
||||
void ShibbolethCredentials::removeShibCookie()
|
||||
{
|
||||
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
||||
job->setSettings(_account->settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
job->setSettings(Utility::settingsWithGroup(Theme::instance()->appName(), job).release());
|
||||
job->setKey(keychainKey(_account->url().toString(), "shibAssertion"));
|
||||
job->start();
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ public:
|
||||
ShibbolethCredentials(const QNetworkCookie &cookie);
|
||||
|
||||
void setAccount(Account* account) Q_DECL_OVERRIDE;
|
||||
bool changed(AbstractCredentials* credentials) const Q_DECL_OVERRIDE;
|
||||
QString authType() const Q_DECL_OVERRIDE;
|
||||
QString user() const Q_DECL_OVERRIDE;
|
||||
QNetworkAccessManager* getQNAM() const Q_DECL_OVERRIDE;
|
||||
|
||||
@@ -81,7 +81,7 @@ Folder::Folder(const FolderDefinition& definition,
|
||||
|
||||
_syncResult.setFolder(_definition.alias);
|
||||
|
||||
_engine.reset(new SyncEngine(_accountState->account(), path(), remoteUrl(), remotePath(), &_journal));
|
||||
_engine.reset(new SyncEngine(_accountState->account(), path(), remotePath(), &_journal));
|
||||
// pass the setting if hidden files are to be ignored, will be read in csync_update
|
||||
_engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles);
|
||||
|
||||
@@ -225,7 +225,7 @@ QString Folder::remotePath() const
|
||||
|
||||
QUrl Folder::remoteUrl() const
|
||||
{
|
||||
return Account::concatUrlPath(_accountState->account()->davUrl(), remotePath());
|
||||
return Utility::concatUrlPath(_accountState->account()->davUrl(), remotePath());
|
||||
}
|
||||
|
||||
bool Folder::syncPaused() const
|
||||
|
||||
+31
-9
@@ -191,7 +191,7 @@ int FolderMan::setupFolders()
|
||||
{
|
||||
unloadAndDeleteAllFolders();
|
||||
|
||||
auto settings = Account::settingsWithGroup(QLatin1String("Accounts"));
|
||||
auto settings = Utility::settingsWithGroup(QLatin1String("Accounts"));
|
||||
const auto accountsWithSettings = settings->childGroups();
|
||||
if (accountsWithSettings.isEmpty()) {
|
||||
int r = setupFoldersMigration();
|
||||
@@ -338,6 +338,7 @@ QString FolderMan::unescapeAlias( const QString& alias )
|
||||
|
||||
// filename is the name of the file only, it does not include
|
||||
// the configuration directory path
|
||||
// WARNING: Do not remove this code, it is used for predefined/automated deployments (2016)
|
||||
Folder* FolderMan::setupFolderFromOldConfigFile(const QString &file, AccountState *accountState )
|
||||
{
|
||||
Folder *folder = 0;
|
||||
@@ -408,6 +409,8 @@ Folder* FolderMan::setupFolderFromOldConfigFile(const QString &file, AccountStat
|
||||
//migrate settings
|
||||
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
|
||||
settings.remove(QLatin1String("blackList"));
|
||||
// FIXME: If you remove this codepath, you need to provide another way to do
|
||||
// this via theme.h or the normal FolderMan::setupFolders
|
||||
}
|
||||
|
||||
folder->saveToSettings();
|
||||
@@ -517,6 +520,26 @@ void FolderMan::scheduleFolder( Folder *f )
|
||||
startScheduledSyncSoon();
|
||||
}
|
||||
|
||||
void FolderMan::scheduleFolderNext(Folder* f)
|
||||
{
|
||||
auto alias = f->alias();
|
||||
qDebug() << "Schedule folder " << alias << " to sync! Front-of-queue.";
|
||||
|
||||
if( !f->canSync() ) {
|
||||
qDebug() << "Folder is not ready to sync, not scheduled!";
|
||||
return;
|
||||
}
|
||||
|
||||
_scheduledFolders.removeAll(f);
|
||||
|
||||
f->prepareToSync();
|
||||
emit folderSyncStateChange(f);
|
||||
_scheduledFolders.prepend(f);
|
||||
emit scheduleQueueChanged();
|
||||
|
||||
startScheduledSyncSoon();
|
||||
}
|
||||
|
||||
void FolderMan::slotScheduleETagJob(const QString &/*alias*/, RequestEtagJob *job)
|
||||
{
|
||||
QObject::connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotEtagJobDestroyed(QObject*)));
|
||||
@@ -670,12 +693,11 @@ void FolderMan::slotStartScheduledFolderSync()
|
||||
}
|
||||
|
||||
// Find the first folder in the queue that can be synced.
|
||||
Folder* f = 0;
|
||||
Folder* folder = 0;
|
||||
while( !_scheduledFolders.isEmpty() ) {
|
||||
f = _scheduledFolders.dequeue();
|
||||
Q_ASSERT(f);
|
||||
|
||||
if( f->canSync() ) {
|
||||
Folder* g = _scheduledFolders.dequeue();
|
||||
if( g->canSync() ) {
|
||||
folder = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -683,9 +705,9 @@ void FolderMan::slotStartScheduledFolderSync()
|
||||
emit scheduleQueueChanged();
|
||||
|
||||
// Start syncing this folder!
|
||||
if( f ) {
|
||||
_currentSyncFolder = f;
|
||||
f->startSync( QStringList() );
|
||||
if( folder ) {
|
||||
_currentSyncFolder = folder;
|
||||
folder->startSync( QStringList() );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-6
@@ -161,12 +161,22 @@ public:
|
||||
/** Queues a folder for syncing. */
|
||||
void scheduleFolder(Folder*);
|
||||
|
||||
/** Puts a folder in the very front of the queue. */
|
||||
void scheduleFolderNext(Folder*);
|
||||
|
||||
/** Queues all folders for syncing. */
|
||||
void scheduleAllFolders();
|
||||
|
||||
void setDirtyProxy(bool value = true);
|
||||
void setDirtyNetworkLimits();
|
||||
|
||||
/**
|
||||
* Terminates the current folder sync.
|
||||
*
|
||||
* It does not switch the folder to paused state.
|
||||
*/
|
||||
void terminateSyncProcess();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* signal to indicate a folder has changed its sync state.
|
||||
@@ -247,12 +257,6 @@ private slots:
|
||||
void slotScheduleFolderByTime();
|
||||
|
||||
private:
|
||||
/**
|
||||
* Terminates the current folder sync.
|
||||
*
|
||||
* It does not switch the folder to paused state.
|
||||
*/
|
||||
void terminateSyncProcess();
|
||||
|
||||
/** Adds a new folder, does not add it to the account settings and
|
||||
* does not set an account on the new folder.
|
||||
|
||||
@@ -295,7 +295,11 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &
|
||||
pBRect.setHeight(barHeight);
|
||||
pBRect.setWidth( overallWidth - 2 * margin );
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5,0,0)
|
||||
QStyleOptionProgressBarV2 pBarOpt;
|
||||
#else
|
||||
QStyleOptionProgressBar pBarOpt;
|
||||
#endif
|
||||
|
||||
pBarOpt.state = option.state | QStyle::State_Horizontal;
|
||||
pBarOpt.minimum = 0;
|
||||
|
||||
@@ -38,6 +38,12 @@ 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)
|
||||
{
|
||||
@@ -58,7 +64,6 @@ 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;
|
||||
@@ -69,6 +74,14 @@ 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();
|
||||
}
|
||||
@@ -375,12 +388,13 @@ QModelIndex FolderStatusModel::indexForPath(Folder *f, const QString& path) cons
|
||||
if (slashPos == -1) {
|
||||
// first level folder
|
||||
for (int i = 0; i < _folders.size(); ++i) {
|
||||
if (_folders.at(i)._folder == f) {
|
||||
auto& info = _folders.at(i);
|
||||
if (info._folder == f) {
|
||||
if( path.isEmpty() ) { // the folder object
|
||||
return index(i, 0);
|
||||
}
|
||||
for (int j = 0; j < _folders.at(i)._subs.size(); ++j) {
|
||||
const QString subName = _folders.at(i)._subs.at(j)._name;
|
||||
for (int j = 0; j < info._subs.size(); ++j) {
|
||||
const QString subName = info._subs.at(j)._name;
|
||||
if (subName == path) {
|
||||
return index(j, 0, index(i));
|
||||
}
|
||||
@@ -792,25 +806,6 @@ void FolderStatusModel::slotApplySelectiveSync()
|
||||
resetFolders();
|
||||
}
|
||||
|
||||
static QString shortenFilename( Folder *f, const QString& file )
|
||||
{
|
||||
// strip off the server prefix from the file name
|
||||
QString shortFile(file);
|
||||
if( shortFile.isEmpty() ) {
|
||||
return QString::null;
|
||||
}
|
||||
|
||||
if(shortFile.startsWith(QLatin1String("ownclouds://")) ||
|
||||
shortFile.startsWith(QLatin1String("owncloud://")) ) {
|
||||
// rip off the whole ownCloud URL.
|
||||
if( f ) {
|
||||
QString remotePathUrl = f->remoteUrl().toString();
|
||||
shortFile.remove(Utility::toCSyncScheme(remotePathUrl));
|
||||
}
|
||||
}
|
||||
return shortFile;
|
||||
}
|
||||
|
||||
void FolderStatusModel::slotSetProgress(const ProgressInfo &progress)
|
||||
{
|
||||
auto par = qobject_cast<QWidget*>(QObject::parent());
|
||||
@@ -884,7 +879,7 @@ void FolderStatusModel::slotSetProgress(const ProgressInfo &progress)
|
||||
curItemProgress = curItem._size;
|
||||
}
|
||||
|
||||
QString itemFileName = shortenFilename(f, curItem._file);
|
||||
QString itemFileName = curItem._file;
|
||||
QString kindString = Progress::asActionString(curItem);
|
||||
|
||||
QString fileProgressString;
|
||||
@@ -986,10 +981,12 @@ 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.
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
pi = SubFolderInfo::Progress();
|
||||
} else if (state == SyncResult::NotYetStarted) {
|
||||
FolderMan* folderMan = FolderMan::instance();
|
||||
int pos = folderMan->scheduleQueue().indexOf(f);
|
||||
@@ -1003,16 +1000,16 @@ void FolderStatusModel::slotFolderSyncStateChange(Folder *f)
|
||||
} else {
|
||||
message = tr("Waiting for %n other folder(s)...", "", pos);
|
||||
}
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress._overallSyncString = message;
|
||||
pi = SubFolderInfo::Progress();
|
||||
pi._overallSyncString = message;
|
||||
} else if (state == SyncResult::SyncPrepare) {
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
_folders[folderIndex]._progress._overallSyncString = tr("Preparing to sync...");
|
||||
pi = SubFolderInfo::Progress();
|
||||
pi._overallSyncString = tr("Preparing to sync...");
|
||||
} else if (state == SyncResult::Problem || state == SyncResult::Success) {
|
||||
// Reset the progress info after a sync.
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
pi = SubFolderInfo::Progress();
|
||||
} else if (state == SyncResult::Error) {
|
||||
_folders[folderIndex]._progress = SubFolderInfo::Progress();
|
||||
pi = SubFolderInfo::Progress();
|
||||
}
|
||||
|
||||
// update the icon etc. now
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#include "winuser.h"
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "owncloud.exe.manifest-mingw"
|
||||
@@ -67,7 +67,7 @@ void OcsJob::start()
|
||||
req.setRawHeader("Ocs-APIREQUEST", "true");
|
||||
req.setRawHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
QUrl url = Account::concatUrlPath(account()->url(), path());
|
||||
QUrl url = Utility::concatUrlPath(account()->url(), path());
|
||||
QBuffer *buffer = new QBuffer;
|
||||
|
||||
if (_verb == "GET") {
|
||||
@@ -108,7 +108,7 @@ bool OcsJob::finished()
|
||||
if (!success) {
|
||||
qDebug() << "Could not parse reply to"
|
||||
<< _verb
|
||||
<< Account::concatUrlPath(account()->url(), path())
|
||||
<< Utility::concatUrlPath(account()->url(), path())
|
||||
<< _params
|
||||
<< ":" << replyData;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ bool OcsJob::finished()
|
||||
if (!_passStatusCodes.contains(statusCode)) {
|
||||
qDebug() << "Reply to"
|
||||
<< _verb
|
||||
<< Account::concatUrlPath(account()->url(), path())
|
||||
<< Utility::concatUrlPath(account()->url(), path())
|
||||
<< _params
|
||||
<< "has unexpected status code:" << statusCode << replyData;
|
||||
emit ocsError(statusCode, message);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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>
|
||||
@@ -160,7 +160,7 @@ void ownCloudGui::slotOpenSettingsDialog()
|
||||
}
|
||||
} else {
|
||||
qDebug() << "No configured folders yet, starting setup wizard";
|
||||
OwncloudSetupWizard::runWizard(qApp, SLOT(slotownCloudWizardDone(int)));
|
||||
slotNewAccountWizard();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,6 +600,9 @@ void ownCloudGui::updateContextMenu()
|
||||
_contextMenu->addMenu(_recentActionsMenu);
|
||||
_contextMenu->addSeparator();
|
||||
}
|
||||
if (accountList.isEmpty()) {
|
||||
_contextMenu->addAction(_actionNewAccountWizard);
|
||||
}
|
||||
_contextMenu->addAction(_actionSettings);
|
||||
if (!Theme::instance()->helpUrl().isEmpty()) {
|
||||
_contextMenu->addAction(_actionHelp);
|
||||
@@ -724,11 +727,13 @@ void ownCloudGui::setupActions()
|
||||
_actionStatus = new QAction(tr("Unknown status"), this);
|
||||
_actionStatus->setEnabled( false );
|
||||
_actionSettings = new QAction(tr("Settings..."), this);
|
||||
_actionNewAccountWizard = new QAction(tr("New account..."), this);
|
||||
_actionRecent = new QAction(tr("Details..."), this);
|
||||
_actionRecent->setEnabled( true );
|
||||
|
||||
QObject::connect(_actionRecent, SIGNAL(triggered(bool)), SLOT(slotShowSyncProtocol()));
|
||||
QObject::connect(_actionSettings, SIGNAL(triggered(bool)), SLOT(slotShowSettings()));
|
||||
QObject::connect(_actionNewAccountWizard, SIGNAL(triggered(bool)), SLOT(slotNewAccountWizard()));
|
||||
_actionHelp = new QAction(tr("Help"), this);
|
||||
QObject::connect(_actionHelp, SIGNAL(triggered(bool)), SLOT(slotHelp()));
|
||||
_actionQuit = new QAction(tr("Quit %1").arg(Theme::instance()->appNameGUI()), this);
|
||||
@@ -891,6 +896,11 @@ void ownCloudGui::slotPauseAllFolders()
|
||||
setPauseOnAllFoldersHelper(true);
|
||||
}
|
||||
|
||||
void ownCloudGui::slotNewAccountWizard()
|
||||
{
|
||||
OwncloudSetupWizard::runWizard(qApp, SLOT(slotownCloudWizardDone(int)));
|
||||
}
|
||||
|
||||
void ownCloudGui::setPauseOnAllFoldersHelper(bool pause)
|
||||
{
|
||||
QList<AccountState*> accounts;
|
||||
@@ -904,6 +914,9 @@ void ownCloudGui::setPauseOnAllFoldersHelper(bool pause)
|
||||
foreach (Folder* f, FolderMan::instance()->map()) {
|
||||
if (accounts.contains(f->accountState())) {
|
||||
f->setSyncPaused(pause);
|
||||
if (pause) {
|
||||
f->slotTerminateSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ private slots:
|
||||
void slotLogout();
|
||||
void slotUnpauseAllFolders();
|
||||
void slotPauseAllFolders();
|
||||
void slotNewAccountWizard();
|
||||
|
||||
private:
|
||||
void setPauseOnAllFoldersHelper(bool pause);
|
||||
@@ -124,6 +125,7 @@ private:
|
||||
QAction *_actionLogin;
|
||||
QAction *_actionLogout;
|
||||
|
||||
QAction *_actionNewAccountWizard;
|
||||
QAction *_actionSettings;
|
||||
QAction *_actionStatus;
|
||||
QAction *_actionEstimate;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "sslerrordialog.h"
|
||||
#include "accountmanager.h"
|
||||
#include "clientproxy.h"
|
||||
#include "filesystem.h"
|
||||
|
||||
#include "creds/credentialsfactory.h"
|
||||
#include "creds/abstractcredentials.h"
|
||||
@@ -202,10 +203,34 @@ void OwncloudSetupWizard::slotOwnCloudFoundAuth(const QUrl& url, const QVariantM
|
||||
|
||||
void OwncloudSetupWizard::slotNoOwnCloudFoundAuth(QNetworkReply *reply)
|
||||
{
|
||||
_ocWizard->displayError(tr("Failed to connect to %1 at %2:<br/>%3")
|
||||
.arg(Theme::instance()->appNameGUI(),
|
||||
reply->url().toString(),
|
||||
reply->errorString()), checkDowngradeAdvised(reply));
|
||||
int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
QString contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
|
||||
|
||||
// Do this early because reply might be deleted in message box event loop
|
||||
QString msg = tr("Failed to connect to %1 at %2:<br/>%3")
|
||||
.arg(Theme::instance()->appNameGUI(),
|
||||
reply->url().toString(),
|
||||
reply->errorString());
|
||||
bool isDowngradeAdvised = checkDowngradeAdvised(reply);
|
||||
|
||||
// If a client cert is needed, nginx sends:
|
||||
// 400 "<html>\r\n<head><title>400 No required SSL certificate was sent</title></head>\r\n<body bgcolor=\"white\">\r\n<center><h1>400 Bad Request</h1></center>\r\n<center>No required SSL certificate was sent</center>\r\n<hr><center>nginx/1.10.0</center>\r\n</body>\r\n</html>\r\n"
|
||||
// If the IP needs to be added as "trusted domain" in oC, oC sends:
|
||||
// https://gist.github.com/guruz/ab6d11df1873c2ad3932180de92e7d82
|
||||
if (resultCode != 200 && contentType.startsWith("text/")) {
|
||||
// FIXME: Synchronous dialogs are not so nice because of event loop recursion
|
||||
// (we already create a dialog further below)
|
||||
QString serverError = reply->peek(1024*20);
|
||||
qDebug() << serverError;
|
||||
QMessageBox messageBox(_ocWizard);
|
||||
messageBox.setText(serverError);
|
||||
messageBox.addButton(QMessageBox::Ok);
|
||||
messageBox.setTextFormat(Qt::RichText);
|
||||
messageBox.exec();
|
||||
}
|
||||
|
||||
// Displays message inside wizard and possibly also another message box
|
||||
_ocWizard->displayError(msg, isDowngradeAdvised);
|
||||
|
||||
// Allow the credentials dialog to pop up again for the same URL.
|
||||
// Maybe the user just clicked 'Cancel' by accident or changed his mind.
|
||||
@@ -340,8 +365,8 @@ void OwncloudSetupWizard::slotCreateLocalAndRemoteFolders(const QString& localFo
|
||||
} else {
|
||||
QString res = tr("Creating local sync folder %1...").arg(localFolder);
|
||||
if( fi.mkpath( localFolder ) ) {
|
||||
FileSystem::setFolderMinimumPermissions(localFolder);
|
||||
Utility::setupFavLink( localFolder );
|
||||
// FIXME: Create a local sync folder.
|
||||
res += tr("ok");
|
||||
} else {
|
||||
res += tr("failed.");
|
||||
|
||||
@@ -99,6 +99,8 @@ ProtocolWidget::ProtocolWidget(QWidget *parent) :
|
||||
_issueItemView->setRootIsDecorated(false);
|
||||
_issueItemView->setTextElideMode(Qt::ElideMiddle);
|
||||
_issueItemView->header()->setObjectName("ActivityErrorListHeader");
|
||||
connect(_issueItemView, SIGNAL(itemActivated(QTreeWidgetItem*,int)),
|
||||
SLOT(slotOpenFile(QTreeWidgetItem*,int)));
|
||||
}
|
||||
|
||||
ProtocolWidget::~ProtocolWidget()
|
||||
|
||||
@@ -345,12 +345,12 @@ QSharedPointer<LinkShare> ShareManager::parseLinkShare(const QVariantMap &data)
|
||||
url = QUrl(data.value("url").toString());
|
||||
} else if (_account->serverVersionInt() >= (8 << 16)) {
|
||||
// From ownCloud server version 8 on, a different share link scheme is used.
|
||||
url = QUrl(Account::concatUrlPath(_account->url(), QString("index.php/s/%1").arg(data.value("token").toString())).toString());
|
||||
url = QUrl(Utility::concatUrlPath(_account->url(), QString("index.php/s/%1").arg(data.value("token").toString())).toString());
|
||||
} else {
|
||||
QList<QPair<QString, QString>> queryArgs;
|
||||
queryArgs.append(qMakePair(QString("service"), QString("files")));
|
||||
queryArgs.append(qMakePair(QString("t"), data.value("token").toString()));
|
||||
url = QUrl(Account::concatUrlPath(_account->url(), QLatin1String("public.php"), queryArgs).toString());
|
||||
url = QUrl(Utility::concatUrlPath(_account->url(), QLatin1String("public.php"), queryArgs).toString());
|
||||
}
|
||||
|
||||
QDate expireDate;
|
||||
|
||||
@@ -256,6 +256,7 @@ void OwncloudSetupPage::setErrorString( const QString& err, bool retryHTTPonly )
|
||||
|
||||
OwncloudConnectionMethodDialog dialog;
|
||||
dialog.setUrl(url);
|
||||
// FIXME: Synchronous dialogs are not so nice because of event loop recursion
|
||||
int retVal = dialog.exec();
|
||||
|
||||
switch (retVal) {
|
||||
|
||||
@@ -50,6 +50,8 @@ set(libsync_SRCS
|
||||
propagatorjobs.cpp
|
||||
propagatedownload.cpp
|
||||
propagateupload.cpp
|
||||
propagateuploadv1.cpp
|
||||
propagateuploadng.cpp
|
||||
propagateremotedelete.cpp
|
||||
propagateremotemove.cpp
|
||||
propagateremotemkdir.cpp
|
||||
|
||||
@@ -64,6 +64,10 @@ 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
|
||||
|
||||
+14
-83
@@ -40,7 +40,6 @@ Account::Account(QObject *parent)
|
||||
: QObject(parent)
|
||||
, _capabilities(QVariantMap())
|
||||
, _davPath( Theme::instance()->webDavPath() )
|
||||
, _wasMigrated(false)
|
||||
{
|
||||
qRegisterMetaType<AccountPtr>("AccountPtr");
|
||||
}
|
||||
@@ -77,11 +76,19 @@ AccountPtr Account::sharedFromThis()
|
||||
return _sharedThis.toStrongRef();
|
||||
}
|
||||
|
||||
QString Account::davUser() const
|
||||
{
|
||||
return _davUser.isEmpty() ? _credentials->user() : _davUser;
|
||||
}
|
||||
|
||||
void Account::setDavUser(const QString &newDavUser)
|
||||
{
|
||||
_davUser = newDavUser;
|
||||
}
|
||||
|
||||
QString Account::displayName() const
|
||||
{
|
||||
auto user = _credentials->user();
|
||||
QString dn = QString("%1@%2").arg(user, _url.host());
|
||||
QString dn = QString("%1@%2").arg(davUser(), _url.host());
|
||||
int port = url().port();
|
||||
if (port > 0 && port != 80 && port != 443) {
|
||||
dn.append(QLatin1Char(':'));
|
||||
@@ -95,30 +102,6 @@ QString Account::id() const
|
||||
return _id;
|
||||
}
|
||||
|
||||
static bool isEqualExceptProtocol(const QUrl &url1, const QUrl &url2)
|
||||
{
|
||||
return (url1.host() != url2.host() ||
|
||||
url1.port() != url2.port() ||
|
||||
url1.path() != url2.path());
|
||||
}
|
||||
|
||||
bool Account::changed(AccountPtr other, bool ignoreUrlProtocol) const
|
||||
{
|
||||
if (!other) {
|
||||
return false;
|
||||
}
|
||||
bool changes = false;
|
||||
if (ignoreUrlProtocol) {
|
||||
changes = isEqualExceptProtocol(_url, other->_url);
|
||||
} else {
|
||||
changes = (_url == other->_url);
|
||||
}
|
||||
|
||||
changes |= _credentials->changed(other->credentials());
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
AbstractCredentials *Account::credentials() const
|
||||
{
|
||||
return _credentials.data();
|
||||
@@ -158,12 +141,7 @@ void Account::setCredentials(AbstractCredentials *cred)
|
||||
|
||||
QUrl Account::davUrl() const
|
||||
{
|
||||
return concatUrlPath(url(), davPath());
|
||||
}
|
||||
|
||||
QList<QNetworkCookie> Account::lastAuthCookies() const
|
||||
{
|
||||
return _am->cookieJar()->cookiesForUrl(_url);
|
||||
return Utility::concatUrlPath(url(), davPath());
|
||||
}
|
||||
|
||||
void Account::clearCookieJar()
|
||||
@@ -210,7 +188,7 @@ QNetworkAccessManager *Account::networkAccessManager()
|
||||
|
||||
QNetworkReply *Account::headRequest(const QString &relPath)
|
||||
{
|
||||
return headRequest(concatUrlPath(url(), relPath));
|
||||
return headRequest(Utility::concatUrlPath(url(), relPath));
|
||||
}
|
||||
|
||||
QNetworkReply *Account::headRequest(const QUrl &url)
|
||||
@@ -224,7 +202,7 @@ QNetworkReply *Account::headRequest(const QUrl &url)
|
||||
|
||||
QNetworkReply *Account::getRequest(const QString &relPath)
|
||||
{
|
||||
return getRequest(concatUrlPath(url(), relPath));
|
||||
return getRequest(Utility::concatUrlPath(url(), relPath));
|
||||
}
|
||||
|
||||
QNetworkReply *Account::getRequest(const QUrl &url)
|
||||
@@ -247,7 +225,7 @@ QNetworkReply *Account::deleteRequest( const QUrl &url)
|
||||
|
||||
QNetworkReply *Account::davRequest(const QByteArray &verb, const QString &relPath, QNetworkRequest req, QIODevice *data)
|
||||
{
|
||||
return davRequest(verb, concatUrlPath(davUrl(), relPath), req, data);
|
||||
return davRequest(verb, Utility::concatUrlPath(davUrl(), relPath), req, data);
|
||||
}
|
||||
|
||||
QNetworkReply *Account::davRequest(const QByteArray &verb, const QUrl &url, QNetworkRequest req, QIODevice *data)
|
||||
@@ -341,43 +319,6 @@ void Account::setUrl(const QUrl &url)
|
||||
_url = url;
|
||||
}
|
||||
|
||||
QUrl Account::concatUrlPath(const QUrl &url, const QString &concatPath,
|
||||
const QList< QPair<QString, QString> > &queryItems)
|
||||
{
|
||||
QString path = url.path();
|
||||
if (! concatPath.isEmpty()) {
|
||||
// avoid '//'
|
||||
if (path.endsWith('/') && concatPath.startsWith('/')) {
|
||||
path.chop(1);
|
||||
} // avoid missing '/'
|
||||
else if (!path.endsWith('/') && !concatPath.startsWith('/')) {
|
||||
path += QLatin1Char('/');
|
||||
}
|
||||
path += concatPath; // put the complete path together
|
||||
}
|
||||
|
||||
QUrl tmpUrl = url;
|
||||
tmpUrl.setPath(path);
|
||||
if( queryItems.size() > 0 ) {
|
||||
tmpUrl.setQueryItems(queryItems);
|
||||
}
|
||||
return tmpUrl;
|
||||
}
|
||||
|
||||
QString Account::_configFileName;
|
||||
|
||||
std::unique_ptr<QSettings> Account::settingsWithGroup(const QString& group, QObject *parent)
|
||||
{
|
||||
if (_configFileName.isEmpty()) {
|
||||
// cache file name
|
||||
ConfigFile cfg;
|
||||
_configFileName = cfg.configFile();
|
||||
}
|
||||
std::unique_ptr<QSettings> settings(new QSettings(_configFileName, QSettings::IniFormat, parent));
|
||||
settings->beginGroup(group);
|
||||
return settings;
|
||||
}
|
||||
|
||||
QVariant Account::credentialSetting(const QString &key) const
|
||||
{
|
||||
if (_credentials) {
|
||||
@@ -476,16 +417,6 @@ void Account::handleInvalidCredentials()
|
||||
emit invalidCredentials();
|
||||
}
|
||||
|
||||
bool Account::wasMigrated()
|
||||
{
|
||||
return _wasMigrated;
|
||||
}
|
||||
|
||||
void Account::setMigrated(bool mig)
|
||||
{
|
||||
_wasMigrated = mig;
|
||||
}
|
||||
|
||||
const Capabilities &Account::capabilities() const
|
||||
{
|
||||
return _capabilities;
|
||||
|
||||
+46
-45
@@ -57,10 +57,37 @@ public:
|
||||
/**
|
||||
* @brief The Account class represents an account on an ownCloud Server
|
||||
* @ingroup libsync
|
||||
*
|
||||
* The Account has a name and url. It also has information about credentials,
|
||||
* SSL errors and certificates.
|
||||
*/
|
||||
class OWNCLOUDSYNC_EXPORT Account : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static AccountPtr create();
|
||||
~Account();
|
||||
|
||||
AccountPtr sharedFromThis();
|
||||
|
||||
/**
|
||||
* The user that can be used in dav url.
|
||||
*
|
||||
* This can very well be different frome the login user that's
|
||||
* stored in credentials()->user().
|
||||
*/
|
||||
QString davUser() const;
|
||||
void setDavUser(const QString &newDavUser);
|
||||
|
||||
/// The name of the account as shown in the toolbar
|
||||
QString displayName() const;
|
||||
|
||||
/// The internal id of the account.
|
||||
QString id() const;
|
||||
|
||||
/** Server url of the account */
|
||||
void setUrl(const QUrl &url);
|
||||
QUrl url() const { return _url; }
|
||||
|
||||
/**
|
||||
* @brief The possibly themed dav path for the account. It has
|
||||
* a trailing slash.
|
||||
@@ -70,44 +97,15 @@ public:
|
||||
void setDavPath(const QString&s) { _davPath = s; }
|
||||
void setNonShib(bool nonShib);
|
||||
|
||||
static AccountPtr create();
|
||||
~Account();
|
||||
|
||||
void setSharedThis(AccountPtr sharedThis);
|
||||
AccountPtr sharedFromThis();
|
||||
|
||||
/// The name of the account as shown in the toolbar
|
||||
QString displayName() const;
|
||||
|
||||
/// The internal id of the account.
|
||||
QString id() const;
|
||||
|
||||
/**
|
||||
* @brief Checks the Account instance is different from @param other
|
||||
*
|
||||
* @returns true, if credentials or url have changed, false otherwise
|
||||
*/
|
||||
bool changed(AccountPtr other, bool ignoreUrlProtocol) const;
|
||||
/** Returns webdav entry URL, based on url() */
|
||||
QUrl davUrl() const;
|
||||
|
||||
/** Holds the accounts credentials */
|
||||
AbstractCredentials* credentials() const;
|
||||
void setCredentials(AbstractCredentials *cred);
|
||||
|
||||
/** Server url of the account */
|
||||
void setUrl(const QUrl &url);
|
||||
QUrl url() const { return _url; }
|
||||
|
||||
/** Returns webdav entry URL, based on url() */
|
||||
QUrl davUrl() const;
|
||||
|
||||
/** set and retrieve the migration flag: if an account of a branded
|
||||
* client was migrated from a former ownCloud Account, this is true
|
||||
*/
|
||||
void setMigrated(bool mig);
|
||||
bool wasMigrated();
|
||||
|
||||
QList<QNetworkCookie> lastAuthCookies() const;
|
||||
|
||||
// For creating various network requests
|
||||
QNetworkReply* headRequest(const QString &relPath);
|
||||
QNetworkReply* headRequest(const QUrl &url);
|
||||
QNetworkReply* getRequest(const QString &relPath);
|
||||
@@ -116,6 +114,7 @@ public:
|
||||
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);
|
||||
|
||||
|
||||
/** The ssl configuration during the first connection */
|
||||
QSslConfiguration getOrCreateSslConfig();
|
||||
QSslConfiguration sslConfiguration() const { return _sslConfiguration; }
|
||||
@@ -139,25 +138,21 @@ public:
|
||||
// pluggable handler
|
||||
void setSslErrorHandler(AbstractSslErrorHandler *handler);
|
||||
|
||||
// static helper function
|
||||
static QUrl concatUrlPath(const QUrl &url, const QString &concatPath,
|
||||
const QList< QPair<QString, QString> > &queryItems = (QList<QPair<QString, QString>>()));
|
||||
|
||||
/** Returns a new settings pre-set in a specific group. The Settings will be created
|
||||
with the given parent. If no parent is specified, the caller must destroy the settings */
|
||||
static std::unique_ptr<QSettings> settingsWithGroup(const QString& group, QObject* parent = 0);
|
||||
|
||||
// to be called by credentials only
|
||||
// To be called by credentials only, for storing username and the like
|
||||
QVariant credentialSetting(const QString& key) const;
|
||||
void setCredentialSetting(const QString& key, const QVariant &value);
|
||||
|
||||
/** Assign a client certificate */
|
||||
void setCertificate(const QByteArray certficate = QByteArray(), const QString privateKey = QString());
|
||||
|
||||
void setCapabilities(const QVariantMap &caps);
|
||||
/** Access the server capabilities */
|
||||
const Capabilities &capabilities() const;
|
||||
void setServerVersion(const QString &version);
|
||||
void setCapabilities(const QVariantMap &caps);
|
||||
|
||||
/** Access the server version */
|
||||
QString serverVersion() const;
|
||||
int serverVersionInt() const;
|
||||
void setServerVersion(const QString &version);
|
||||
|
||||
/** Whether the server is too old.
|
||||
*
|
||||
@@ -172,6 +167,7 @@ public:
|
||||
bool serverVersionUnsupported() const;
|
||||
|
||||
// Fixed from 8.1 https://github.com/owncloud/client/issues/3730
|
||||
/** Detects a specific bug in older server versions */
|
||||
bool rootEtagChangesNotOnlySubFolderEtags();
|
||||
|
||||
void clearCookieJar();
|
||||
@@ -180,12 +176,16 @@ public:
|
||||
void resetNetworkAccessManager();
|
||||
QNetworkAccessManager* networkAccessManager();
|
||||
|
||||
/// Called by network jobs on credential errors.
|
||||
/// Called by network jobs on credential errors, emits invalidCredentials()
|
||||
void handleInvalidCredentials();
|
||||
|
||||
signals:
|
||||
/// Emitted whenever there's network activity
|
||||
void propagatorNetworkActivity();
|
||||
|
||||
/// Triggered by handleInvalidCredentials()
|
||||
void invalidCredentials();
|
||||
|
||||
void credentialsFetched(AbstractCredentials* credentials);
|
||||
void credentialsAsked(AbstractCredentials* credentials);
|
||||
|
||||
@@ -204,9 +204,11 @@ protected Q_SLOTS:
|
||||
|
||||
private:
|
||||
Account(QObject *parent = 0);
|
||||
void setSharedThis(AccountPtr sharedThis);
|
||||
|
||||
QWeakPointer<Account> _sharedThis;
|
||||
QString _id;
|
||||
QString _davUser;
|
||||
QMap<QString, QVariant> _settingsMap;
|
||||
QUrl _url;
|
||||
QList<QSslCertificate> _approvedCerts;
|
||||
@@ -225,7 +227,6 @@ private:
|
||||
QByteArray _pemCertificate;
|
||||
QString _pemPrivateKey;
|
||||
QString _davPath; // defaults to value from theme, might be overwritten in brandings
|
||||
bool _wasMigrated;
|
||||
friend class AccountManager;
|
||||
};
|
||||
|
||||
|
||||
@@ -108,4 +108,10 @@ QByteArray Capabilities::uploadChecksumType() const
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
bool Capabilities::chunkingNg() const
|
||||
{
|
||||
return _capabilities["dav"].toMap()["chunking"].toByteArray() >= "1.0";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ public:
|
||||
bool sharePublicLinkEnforceExpireDate() const;
|
||||
int sharePublicLinkExpireDateDays() const;
|
||||
bool shareResharing() const;
|
||||
bool chunkingNg() const;
|
||||
|
||||
/// returns true if the capabilities report notifications
|
||||
bool notificationsAvailable() const;
|
||||
|
||||
@@ -230,10 +230,26 @@ void ConnectionValidator::slotCapabilitiesRecieved(const QVariantMap &json)
|
||||
auto caps = json.value("ocs").toMap().value("data").toMap().value("capabilities");
|
||||
qDebug() << "Server capabilities" << caps;
|
||||
_account->setCapabilities(caps.toMap());
|
||||
reportResult(Connected);
|
||||
return;
|
||||
fetchUser();
|
||||
}
|
||||
|
||||
void ConnectionValidator::fetchUser()
|
||||
{
|
||||
|
||||
JsonApiJob *job = new JsonApiJob(_account, QLatin1String("ocs/v1.php/cloud/user"), this);
|
||||
job->setTimeout(timeoutToUseMsec);
|
||||
QObject::connect(job, SIGNAL(jsonReceived(QVariantMap, int)), this, SLOT(slotUserFetched(QVariantMap)));
|
||||
job->start();
|
||||
}
|
||||
|
||||
void ConnectionValidator::slotUserFetched(const QVariantMap &json)
|
||||
{
|
||||
QString user = json.value("ocs").toMap().value("data").toMap().value("id").toString();
|
||||
if (!user.isEmpty()) {
|
||||
_account->setDavUser(user);
|
||||
}
|
||||
reportResult(Connected);
|
||||
}
|
||||
|
||||
void ConnectionValidator::reportResult(Status status)
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace OCC {
|
||||
* This is a job-like class to check that the server is up and that we are connected.
|
||||
* There are two entry points: checkServerAndAuth and checkAuthentication
|
||||
* checkAuthentication is the quick version that only does the propfind
|
||||
* while checkServerAndAuth is doing the 3 calls.
|
||||
* while checkServerAndAuth is doing the 4 calls.
|
||||
*
|
||||
* We cannot use the capabilites call to test the login and the password because of
|
||||
* https://github.com/owncloud/core/issues/12930
|
||||
@@ -61,7 +61,15 @@ namespace OCC {
|
||||
+-> checkServerCapabilities (cloud/capabilities)
|
||||
JsonApiJob
|
||||
|
|
||||
+-> slotCapabilitiesRecieved --> X
|
||||
+-> slotCapabilitiesRecieved -+
|
||||
|
|
||||
+-----------------------------------+
|
||||
|
|
||||
+-> fetchUser
|
||||
PropfindJob
|
||||
|
|
||||
+-> slotUserFetched --> X
|
||||
|
||||
\endcode
|
||||
*/
|
||||
class OWNCLOUDSYNC_EXPORT ConnectionValidator : public QObject
|
||||
@@ -110,10 +118,12 @@ protected slots:
|
||||
void slotAuthSuccess();
|
||||
|
||||
void slotCapabilitiesRecieved(const QVariantMap&);
|
||||
void slotUserFetched(const QVariantMap &);
|
||||
|
||||
private:
|
||||
void reportResult(Status status);
|
||||
void checkServerCapabilities();
|
||||
void fetchUser();
|
||||
|
||||
QStringList _errors;
|
||||
AccountPtr _account;
|
||||
|
||||
@@ -42,7 +42,6 @@ public:
|
||||
*/
|
||||
virtual void setAccount(Account* account);
|
||||
|
||||
virtual bool changed(AbstractCredentials* credentials) const = 0;
|
||||
virtual QString authType() const = 0;
|
||||
virtual QString user() const = 0;
|
||||
virtual QNetworkAccessManager* getQNAM() const = 0;
|
||||
|
||||
@@ -18,13 +18,6 @@
|
||||
namespace OCC
|
||||
{
|
||||
|
||||
bool DummyCredentials::changed(AbstractCredentials* credentials) const
|
||||
{
|
||||
DummyCredentials* dummy(dynamic_cast< DummyCredentials* >(credentials));
|
||||
|
||||
return dummy == 0;
|
||||
}
|
||||
|
||||
QString DummyCredentials::authType() const
|
||||
{
|
||||
return QString::fromLatin1("dummy");
|
||||
|
||||
@@ -28,7 +28,6 @@ public:
|
||||
|
||||
QString _user;
|
||||
QString _password;
|
||||
bool changed(AbstractCredentials* credentials) const Q_DECL_OVERRIDE;
|
||||
QString authType() const Q_DECL_OVERRIDE;
|
||||
QString user() const Q_DECL_OVERRIDE;
|
||||
QNetworkAccessManager* getQNAM() const Q_DECL_OVERRIDE;
|
||||
|
||||
@@ -71,21 +71,6 @@ HttpCredentials::HttpCredentials(const QString& user, const QString& password, c
|
||||
{
|
||||
}
|
||||
|
||||
bool HttpCredentials::changed(AbstractCredentials* credentials) const
|
||||
{
|
||||
HttpCredentials* other(qobject_cast< HttpCredentials* >(credentials));
|
||||
|
||||
if (!other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!other || (other->user() != this->user())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString HttpCredentials::authType() const
|
||||
{
|
||||
return QString::fromLatin1("http");
|
||||
@@ -147,7 +132,7 @@ void HttpCredentials::fetchFromKeychain()
|
||||
_certificatePath = _account->credentialSetting(QLatin1String(certifPathC)).toString();
|
||||
_certificatePasswd = _account->credentialSetting(QLatin1String(certifPasswdC)).toString();
|
||||
|
||||
auto settings = _account->settingsWithGroup(Theme::instance()->appName());
|
||||
auto settings = Utility::settingsWithGroup(Theme::instance()->appName());
|
||||
const QString kck = keychainKey(_account->url().toString(), _user );
|
||||
|
||||
QString key = QString::fromLatin1( "%1/data" ).arg( kck );
|
||||
@@ -229,7 +214,7 @@ void HttpCredentials::invalidateToken()
|
||||
}
|
||||
|
||||
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
||||
auto settings = _account->settingsWithGroup(Theme::instance()->appName());
|
||||
auto settings = Utility::settingsWithGroup(Theme::instance()->appName());
|
||||
settings->setParent(job); // make the job parent to make setting deleted properly
|
||||
job->setSettings(settings.release());
|
||||
job->setInsecureFallback(true);
|
||||
@@ -280,7 +265,7 @@ void HttpCredentials::persist()
|
||||
_account->setCredentialSetting(QLatin1String(certifPathC), _certificatePath);
|
||||
_account->setCredentialSetting(QLatin1String(certifPasswdC), _certificatePasswd);
|
||||
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
||||
auto settings = _account->settingsWithGroup(Theme::instance()->appName());
|
||||
auto settings = Utility::settingsWithGroup(Theme::instance()->appName());
|
||||
settings->setParent(job); // make the job parent to make setting deleted properly
|
||||
job->setSettings(settings.release());
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ public:
|
||||
explicit HttpCredentials();
|
||||
HttpCredentials(const QString& user, const QString& password, const QString& certificatePath, const QString& certificatePasswd);
|
||||
|
||||
bool changed(AbstractCredentials* credentials) const Q_DECL_OVERRIDE;
|
||||
QString authType() const Q_DECL_OVERRIDE;
|
||||
QNetworkAccessManager* getQNAM() const Q_DECL_OVERRIDE;
|
||||
bool ready() const Q_DECL_OVERRIDE;
|
||||
|
||||
@@ -80,17 +80,6 @@ TokenCredentials::TokenCredentials(const QString& user, const QString& password,
|
||||
{
|
||||
}
|
||||
|
||||
bool TokenCredentials::changed(AbstractCredentials* credentials) const
|
||||
{
|
||||
TokenCredentials* other(dynamic_cast< TokenCredentials* >(credentials));
|
||||
|
||||
if (!other || (other->user() != this->user())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QString TokenCredentials::authType() const
|
||||
{
|
||||
return QString::fromLatin1("token");
|
||||
|
||||
@@ -40,7 +40,6 @@ public:
|
||||
TokenCredentials();
|
||||
TokenCredentials(const QString& user, const QString& password, const QString &token);
|
||||
|
||||
bool changed(AbstractCredentials* credentials) const Q_DECL_OVERRIDE;
|
||||
QString authType() const Q_DECL_OVERRIDE;
|
||||
QNetworkAccessManager* getQNAM() const Q_DECL_OVERRIDE;
|
||||
bool ready() const Q_DECL_OVERRIDE;
|
||||
|
||||
@@ -146,6 +146,17 @@ void FileSystem::setFileReadOnly(const QString& filename, bool readonly)
|
||||
file.setPermissions(permissions);
|
||||
}
|
||||
|
||||
void FileSystem::setFolderMinimumPermissions(const QString& filename)
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
QFile::Permissions perm = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
|
||||
QFile file(filename);
|
||||
file.setPermissions(perm);
|
||||
#else
|
||||
Q_UNUSED(filename);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void FileSystem::setFileReadOnlyWeak(const QString& filename, bool readonly)
|
||||
{
|
||||
|
||||
@@ -67,6 +67,12 @@ void OWNCLOUDSYNC_EXPORT setFileReadOnly(const QString& filename, bool readonly)
|
||||
*/
|
||||
void OWNCLOUDSYNC_EXPORT setFileReadOnlyWeak(const QString& filename, bool readonly);
|
||||
|
||||
/**
|
||||
* @brief Try to set permissions so that other users on the local machine can not
|
||||
* go into the folder.
|
||||
*/
|
||||
void OWNCLOUDSYNC_EXPORT setFolderMinimumPermissions(const QString& filename);
|
||||
|
||||
/** convert a "normal" windows path into a path that can be 32k chars long. */
|
||||
QString OWNCLOUDSYNC_EXPORT longWinPath( const QString& inpath );
|
||||
|
||||
|
||||
@@ -106,14 +106,24 @@ MkColJob::MkColJob(AccountPtr account, const QString &path, QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
MkColJob::MkColJob(AccountPtr account, const QUrl &url,
|
||||
const QMap<QByteArray, QByteArray> &extraHeaders, QObject *parent)
|
||||
: AbstractNetworkJob(account, QString(), parent), _url(url), _extraHeaders(extraHeaders)
|
||||
{
|
||||
}
|
||||
|
||||
void MkColJob::start()
|
||||
{
|
||||
// add 'Content-Length: 0' header (see https://github.com/owncloud/client/issues/3256)
|
||||
QNetworkRequest req;
|
||||
req.setRawHeader("Content-Length", "0");
|
||||
for(auto it = _extraHeaders.constBegin(); it != _extraHeaders.constEnd(); ++it) {
|
||||
req.setRawHeader(it.key(), it.value());
|
||||
}
|
||||
|
||||
// assumes ownership
|
||||
QNetworkReply *reply = davRequest("MKCOL", path(), req);
|
||||
QNetworkReply *reply = _url.isValid() ? davRequest("MKCOL", _url, req)
|
||||
: davRequest("MKCOL", path(), req);
|
||||
setReply(reply);
|
||||
setupConnections(reply);
|
||||
AbstractNetworkJob::start();
|
||||
@@ -264,6 +274,11 @@ LsColJob::LsColJob(AccountPtr account, const QString &path, QObject *parent)
|
||||
{
|
||||
}
|
||||
|
||||
LsColJob::LsColJob(AccountPtr account, const QUrl &url, QObject *parent)
|
||||
: AbstractNetworkJob(account, QString(), parent), _url(url)
|
||||
{
|
||||
}
|
||||
|
||||
void LsColJob::setProperties(QList<QByteArray> properties)
|
||||
{
|
||||
_properties = properties;
|
||||
@@ -307,7 +322,8 @@ void LsColJob::start()
|
||||
QBuffer *buf = new QBuffer(this);
|
||||
buf->setData(xml);
|
||||
buf->open(QIODevice::ReadOnly);
|
||||
QNetworkReply *reply = davRequest("PROPFIND", path(), req, buf);
|
||||
QNetworkReply *reply = _url.isValid() ? davRequest("PROPFIND", _url, req, buf)
|
||||
: davRequest("PROPFIND", path(), req, buf);
|
||||
buf->setParent(reply);
|
||||
setReply(reply);
|
||||
setupConnections(reply);
|
||||
@@ -447,7 +463,7 @@ bool CheckServerJob::finished()
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
QByteArray body = reply()->readAll();
|
||||
QByteArray body = reply()->peek(4*1024);
|
||||
int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if( body.isEmpty() || httpStatus != 200) {
|
||||
qDebug() << "error: status.php replied " << httpStatus << body;
|
||||
@@ -680,7 +696,7 @@ void JsonApiJob::start()
|
||||
{
|
||||
QNetworkRequest req;
|
||||
req.setRawHeader("OCS-APIREQUEST", "true");
|
||||
QUrl url = Account::concatUrlPath(account()->url(), path());
|
||||
QUrl url = Utility::concatUrlPath(account()->url(), path());
|
||||
QList<QPair<QString, QString> > params = _additionalParams;
|
||||
params << qMakePair(QString::fromLatin1("format"), QString::fromLatin1("json"));
|
||||
url.setQueryItems(params);
|
||||
|
||||
@@ -62,6 +62,7 @@ class OWNCLOUDSYNC_EXPORT LsColJob : public AbstractNetworkJob {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LsColJob(AccountPtr account, const QString &path, QObject *parent = 0);
|
||||
explicit LsColJob(AccountPtr account, const QUrl &url, QObject *parent = 0);
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
QHash<QString, qint64> _sizes;
|
||||
|
||||
@@ -87,6 +88,7 @@ private slots:
|
||||
|
||||
private:
|
||||
QList<QByteArray> _properties;
|
||||
QUrl _url; // Used instead of path() if the url is specified in the constructor
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -170,8 +172,12 @@ private:
|
||||
*/
|
||||
class OWNCLOUDSYNC_EXPORT MkColJob : public AbstractNetworkJob {
|
||||
Q_OBJECT
|
||||
QUrl _url; // Only used if the constructor taking a url is taken.
|
||||
QMap<QByteArray, QByteArray> _extraHeaders;
|
||||
public:
|
||||
explicit MkColJob(AccountPtr account, const QString &path, QObject *parent = 0);
|
||||
explicit MkColJob(AccountPtr account, const QUrl &url,
|
||||
const QMap<QByteArray, QByteArray> &extraHeaders, QObject *parent = 0);
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
|
||||
signals:
|
||||
|
||||
@@ -270,7 +270,14 @@ PropagateItemJob* OwncloudPropagator::createJob(const SyncFileItemPtr &item) {
|
||||
job->setDeleteExistingFolder(deleteExisting);
|
||||
return job;
|
||||
} else {
|
||||
auto job = new PropagateUploadFile(this, item);
|
||||
PropagateUploadFileCommon *job = 0;
|
||||
static const auto chunkng = qgetenv("OWNCLOUD_CHUNKING_NG");
|
||||
if (item->_size > chunkSize()
|
||||
&& (account()->capabilities().chunkingNg() || chunkng == "1") && chunkng != "0") {
|
||||
job = new PropagateUploadFileNG(this, item);
|
||||
} else {
|
||||
job = new PropagateUploadFileV1(this, item);
|
||||
}
|
||||
job->setDeleteExisting(deleteExisting);
|
||||
return job;
|
||||
}
|
||||
@@ -307,7 +314,7 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
|
||||
|
||||
if (!removedDirectory.isEmpty() && item->_file.startsWith(removedDirectory)) {
|
||||
// this is an item in a directory which is going to be removed.
|
||||
PropagateDirectory *delDirJob = dynamic_cast<PropagateDirectory*>(directoriesToRemove.first());
|
||||
PropagateDirectory *delDirJob = qobject_cast<PropagateDirectory*>(directoriesToRemove.first());
|
||||
|
||||
if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
|
||||
// already taken care of. (by the removal of the parent directory)
|
||||
@@ -405,10 +412,12 @@ void OwncloudPropagator::start(const SyncFileItemVector& items)
|
||||
QTimer::singleShot(0, this, SLOT(scheduleNextJob()));
|
||||
}
|
||||
|
||||
// ownCloud server < 7.0 did not had permissions so we need some other euristics
|
||||
// to detect wrong doing in a Shared directory
|
||||
bool OwncloudPropagator::isInSharedDirectory(const QString& file)
|
||||
{
|
||||
bool re = false;
|
||||
if( _remoteDir.contains( _account->davPath() + QLatin1String("Shared") ) ) {
|
||||
if( _remoteFolder.startsWith( QLatin1String("Shared") ) ) {
|
||||
// The Shared directory is synced as its own sync connection
|
||||
re = true;
|
||||
} else {
|
||||
@@ -606,9 +615,15 @@ bool PropagateDirectory::scheduleNextJob()
|
||||
return false;
|
||||
}
|
||||
|
||||
// cache the value of first unfinished subjob
|
||||
bool stopAtDirectory = false;
|
||||
// FIXME: use the cached value of finished job
|
||||
for (int i = 0; i < _subJobs.count(); ++i) {
|
||||
int i = _firstUnfinishedSubJob;
|
||||
int subJobsCount = _subJobs.count();
|
||||
while (i < subJobsCount && _subJobs.at(i)->_state == Finished) {
|
||||
_firstUnfinishedSubJob = ++i;
|
||||
}
|
||||
|
||||
for (int i = _firstUnfinishedSubJob; i < subJobsCount; ++i) {
|
||||
if (_subJobs.at(i)->_state == Finished) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -195,10 +195,11 @@ 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)
|
||||
, _firstJob(0), _item(item), _jobsFinished(0), _runningNow(0), _hasError(SyncFileItem::NoStatus), _firstUnfinishedSubJob(0)
|
||||
{ }
|
||||
|
||||
virtual ~PropagateDirectory() {
|
||||
@@ -266,8 +267,7 @@ class OwncloudPropagator : public QObject {
|
||||
|
||||
public:
|
||||
const QString _localDir; // absolute path to the local directory. ends with '/'
|
||||
const QString _remoteDir; // path to the root of the remote. ends with '/' (include WebDAV path)
|
||||
const QString _remoteFolder; // folder. (same as remoteDir but without the WebDAV path)
|
||||
const QString _remoteFolder; // remote folder, ends with '/'
|
||||
|
||||
SyncJournalDb * const _journal;
|
||||
bool _finishedEmited; // used to ensure that finished is only emitted once
|
||||
@@ -275,10 +275,8 @@ public:
|
||||
|
||||
public:
|
||||
OwncloudPropagator(AccountPtr account, const QString &localDir,
|
||||
const QString &remoteDir, const QString &remoteFolder,
|
||||
SyncJournalDb *progressDb)
|
||||
const QString &remoteFolder, SyncJournalDb *progressDb)
|
||||
: _localDir((localDir.endsWith(QChar('/'))) ? localDir : localDir+'/' )
|
||||
, _remoteDir((remoteDir.endsWith(QChar('/'))) ? remoteDir : remoteDir+'/' )
|
||||
, _remoteFolder((remoteFolder.endsWith(QChar('/'))) ? remoteFolder : remoteFolder+'/' )
|
||||
, _journal(progressDb)
|
||||
, _finishedEmited(false)
|
||||
@@ -379,10 +377,11 @@ private:
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
|
||||
// access to signals which are protected in Qt4
|
||||
friend class PropagateDownloadFile;
|
||||
friend class PropagateUploadFile;
|
||||
friend class PropagateLocalMkdir;
|
||||
friend class PropagateLocalRename;
|
||||
friend class PropagateRemoteMove;
|
||||
friend class PropagateUploadFileV1;
|
||||
friend class PropagateUploadFileNG;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -44,9 +44,10 @@ QString ownCloudTheme::configFileName() const
|
||||
QString ownCloudTheme::about() const
|
||||
{
|
||||
QString devString;
|
||||
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>"
|
||||
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>"
|
||||
"<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 "
|
||||
|
||||
@@ -593,7 +593,7 @@ void PropagateDownloadFile::deleteExistingFolder()
|
||||
}
|
||||
|
||||
QString conflictDir = FileSystem::makeConflictFileName(
|
||||
existingDir, Utility::qDateTimeFromTime_t(_item->_modtime));
|
||||
existingDir, Utility::qDateTimeFromTime_t(FileSystem::getModTime(existingDir)));
|
||||
|
||||
emit _propagator->touchedFile(existingDir);
|
||||
emit _propagator->touchedFile(conflictDir);
|
||||
@@ -722,7 +722,8 @@ void PropagateDownloadFile::downloadFinished()
|
||||
&& !FileSystem::fileEquals(fn, _tmpFile.fileName());
|
||||
if (isConflict) {
|
||||
QString renameError;
|
||||
QString conflictFileName = FileSystem::makeConflictFileName(fn, Utility::qDateTimeFromTime_t(_item->_modtime));
|
||||
QString conflictFileName = FileSystem::makeConflictFileName(
|
||||
fn, Utility::qDateTimeFromTime_t(FileSystem::getModTime(fn)));
|
||||
if (!FileSystem::rename(fn, conflictFileName, &renameError)) {
|
||||
// If the rename fails, don't replace it.
|
||||
|
||||
|
||||
@@ -22,11 +22,14 @@ DeleteJob::DeleteJob(AccountPtr account, const QString& path, QObject* parent)
|
||||
: AbstractNetworkJob(account, path, parent)
|
||||
{ }
|
||||
|
||||
DeleteJob::DeleteJob(AccountPtr account, const QUrl& url, QObject* parent)
|
||||
: AbstractNetworkJob(account, QString(), parent), _url(url)
|
||||
{ }
|
||||
|
||||
void DeleteJob::start()
|
||||
{
|
||||
QNetworkRequest req;
|
||||
setReply(davRequest("DELETE", path(), req));
|
||||
setReply(_url.isValid() ? davRequest("DELETE", _url, req) : davRequest("DELETE", path(), req));
|
||||
setupConnections(reply());
|
||||
|
||||
if( reply()->error() != QNetworkReply::NoError ) {
|
||||
|
||||
@@ -24,8 +24,10 @@ namespace OCC {
|
||||
*/
|
||||
class DeleteJob : public AbstractNetworkJob {
|
||||
Q_OBJECT
|
||||
QUrl _url; // Only used if the constructor taking a url is taken.
|
||||
public:
|
||||
explicit DeleteJob(AccountPtr account, const QString& path, QObject* parent = 0);
|
||||
explicit DeleteJob(AccountPtr account, const QUrl& url, QObject* parent = 0);
|
||||
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
bool finished() Q_DECL_OVERRIDE;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "filesystem.h"
|
||||
#include <QFile>
|
||||
#include <QStringList>
|
||||
#include <QDir>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
@@ -28,12 +29,20 @@ MoveJob::MoveJob(AccountPtr account, const QString& path,
|
||||
: AbstractNetworkJob(account, path, parent), _destination(destination)
|
||||
{ }
|
||||
|
||||
MoveJob::MoveJob(AccountPtr account, const QUrl& url, const QString &destination,
|
||||
QMap<QByteArray, QByteArray> extraHeaders, QObject* parent)
|
||||
: AbstractNetworkJob(account, QString(), parent), _destination(destination), _url(url)
|
||||
, _extraHeaders(extraHeaders)
|
||||
{ }
|
||||
|
||||
void MoveJob::start()
|
||||
{
|
||||
QNetworkRequest req;
|
||||
req.setRawHeader("Destination", QUrl::toPercentEncoding(_destination, "/"));
|
||||
setReply(davRequest("MOVE", path(), req));
|
||||
for(auto it = _extraHeaders.constBegin(); it != _extraHeaders.constEnd(); ++it) {
|
||||
req.setRawHeader(it.key(), it.value());
|
||||
}
|
||||
setReply(_url.isValid() ? davRequest("MOVE", _url, req) : davRequest("MOVE", path(), req));
|
||||
setupConnections(reply());
|
||||
|
||||
if( reply()->error() != QNetworkReply::NoError ) {
|
||||
@@ -93,10 +102,11 @@ void PropagateRemoteMove::start()
|
||||
}
|
||||
}
|
||||
|
||||
QString destination = QDir::cleanPath(_propagator->account()->url().path() + QLatin1Char('/')
|
||||
+ _propagator->account()->davPath() + _propagator->_remoteFolder + _item->_renameTarget);
|
||||
_job = new MoveJob(_propagator->account(),
|
||||
_propagator->_remoteFolder + _item->_file,
|
||||
_propagator->_remoteDir + _item->_renameTarget,
|
||||
this);
|
||||
destination, this);
|
||||
connect(_job, SIGNAL(finishedSignal()), this, SLOT(slotMoveJobFinished()));
|
||||
_propagator->_activeJobList.append(this);
|
||||
_job->start();
|
||||
|
||||
@@ -25,8 +25,12 @@ namespace OCC {
|
||||
class MoveJob : public AbstractNetworkJob {
|
||||
Q_OBJECT
|
||||
const QString _destination;
|
||||
const QUrl _url; // Only used (instead of path) when the constructor taking an URL is used
|
||||
QMap<QByteArray, QByteArray> _extraHeaders;
|
||||
public:
|
||||
explicit MoveJob(AccountPtr account, const QString& path, const QString &destination, QObject* parent = 0);
|
||||
explicit MoveJob(AccountPtr account, const QUrl& url, const QString &destination,
|
||||
QMap<QByteArray, QByteArray> _extraHeaders, QObject* parent = 0);
|
||||
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
bool finished() Q_DECL_OVERRIDE;
|
||||
|
||||
@@ -72,7 +72,8 @@ void PUTFileJob::start() {
|
||||
req.setRawHeader(it.key(), it.value());
|
||||
}
|
||||
|
||||
setReply(davRequest("PUT", path(), req, _device.data()));
|
||||
setReply(_url.isValid() ? davRequest("PUT", _url, req, _device.data())
|
||||
: davRequest("PUT", path(), req, _device.data()));
|
||||
setupConnections(reply());
|
||||
|
||||
if( reply()->error() != QNetworkReply::NoError ) {
|
||||
@@ -184,7 +185,13 @@ bool PollJob::finished()
|
||||
return true;
|
||||
}
|
||||
|
||||
void PropagateUploadFile::start()
|
||||
void PropagateUploadFileCommon::setDeleteExisting(bool enabled)
|
||||
{
|
||||
_deleteExisting = enabled;
|
||||
}
|
||||
|
||||
|
||||
void PropagateUploadFileCommon::start()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
|
||||
return;
|
||||
@@ -205,7 +212,7 @@ void PropagateUploadFile::start()
|
||||
job->start();
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotComputeContentChecksum()
|
||||
void PropagateUploadFileCommon::slotComputeContentChecksum()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0)) {
|
||||
return;
|
||||
@@ -239,12 +246,7 @@ void PropagateUploadFile::slotComputeContentChecksum()
|
||||
computeChecksum->start(filePath);
|
||||
}
|
||||
|
||||
void PropagateUploadFile::setDeleteExisting(bool enabled)
|
||||
{
|
||||
_deleteExisting = enabled;
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotComputeTransmissionChecksum(const QByteArray& contentChecksumType, const QByteArray& contentChecksum)
|
||||
void PropagateUploadFileCommon::slotComputeTransmissionChecksum(const QByteArray& contentChecksumType, const QByteArray& contentChecksum)
|
||||
{
|
||||
_item->_contentChecksum = contentChecksum;
|
||||
_item->_contentChecksumType = contentChecksumType;
|
||||
@@ -276,7 +278,7 @@ void PropagateUploadFile::slotComputeTransmissionChecksum(const QByteArray& cont
|
||||
computeChecksum->start(filePath);
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum)
|
||||
void PropagateUploadFileCommon::slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum)
|
||||
{
|
||||
// Remove ourselfs from the list of active job, before any posible call to done()
|
||||
// When we start chunks, we will add it again, once for every chunks.
|
||||
@@ -322,23 +324,7 @@ void PropagateUploadFile::slotStartUpload(const QByteArray& transmissionChecksum
|
||||
return;
|
||||
}
|
||||
|
||||
_chunkCount = std::ceil(fileSize/double(chunkSize()));
|
||||
_startChunk = 0;
|
||||
_transferId = qrand() ^ _item->_modtime ^ (_item->_size << 16);
|
||||
|
||||
const SyncJournalDb::UploadInfo progressInfo = _propagator->_journal->getUploadInfo(_item->_file);
|
||||
|
||||
if (progressInfo._valid && Utility::qDateTimeToTime_t(progressInfo._modtime) == _item->_modtime ) {
|
||||
_startChunk = progressInfo._chunk;
|
||||
_transferId = progressInfo._transferid;
|
||||
qDebug() << Q_FUNC_INFO << _item->_file << ": Resuming from chunk " << _startChunk;
|
||||
}
|
||||
|
||||
_currentChunk = 0;
|
||||
_duration.start();
|
||||
|
||||
emit progress(*_item, 0);
|
||||
this->startNextChunk();
|
||||
doStartUpload();
|
||||
}
|
||||
|
||||
UploadDevice::UploadDevice(BandwidthManager *bwm)
|
||||
@@ -476,24 +462,64 @@ void UploadDevice::setChoked(bool b) {
|
||||
}
|
||||
}
|
||||
|
||||
void PropagateUploadFile::startNextChunk()
|
||||
void PropagateUploadFileCommon::startPollJob(const QString& path)
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
|
||||
return;
|
||||
PollJob* job = new PollJob(_propagator->account(), path, _item,
|
||||
_propagator->_journal, _propagator->_localDir, this);
|
||||
connect(job, SIGNAL(finishedSignal()), SLOT(slotPollFinished()));
|
||||
SyncJournalDb::PollInfo info;
|
||||
info._file = _item->_file;
|
||||
info._url = path;
|
||||
info._modtime = _item->_modtime;
|
||||
_propagator->_journal->setPollInfo(info);
|
||||
_propagator->_journal->commit("add poll info");
|
||||
_propagator->_activeJobList.append(this);
|
||||
job->start();
|
||||
}
|
||||
|
||||
if (! _jobs.isEmpty() && _currentChunk + _startChunk >= _chunkCount - 1) {
|
||||
// Don't do parallel upload of chunk if this might be the last chunk because the server cannot handle that
|
||||
// https://github.com/owncloud/core/issues/11106
|
||||
// We return now and when the _jobs are finished we will proceed with the last chunk
|
||||
// NOTE: Some other parts of the code such as slotUploadProgress also assume that the last chunk
|
||||
// is sent last.
|
||||
void PropagateUploadFileCommon::slotPollFinished()
|
||||
{
|
||||
PollJob *job = qobject_cast<PollJob *>(sender());
|
||||
Q_ASSERT(job);
|
||||
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
|
||||
if (job->_item->_status != SyncFileItem::Success) {
|
||||
_finished = true;
|
||||
done(job->_item->_status, job->_item->_errorString);
|
||||
return;
|
||||
}
|
||||
quint64 fileSize = _item->_size;
|
||||
|
||||
finalize();
|
||||
}
|
||||
|
||||
void PropagateUploadFileCommon::slotJobDestroyed(QObject* job)
|
||||
{
|
||||
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job) , _jobs.end());
|
||||
}
|
||||
|
||||
void PropagateUploadFileCommon::abort()
|
||||
{
|
||||
foreach(auto *job, _jobs) {
|
||||
if (job->reply()) {
|
||||
qDebug() << Q_FUNC_INFO << job << this->_item->_file;
|
||||
job->reply()->abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used whenever there is an error occuring and jobs might be in progress
|
||||
void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, const QString &error)
|
||||
{
|
||||
_finished = true;
|
||||
abort();
|
||||
done(status, error);
|
||||
}
|
||||
|
||||
QMap<QByteArray, QByteArray> PropagateUploadFileCommon::headers()
|
||||
{
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
headers["OC-Total-Length"] = QByteArray::number(fileSize);
|
||||
headers["OC-Async"] = "1";
|
||||
headers["OC-Chunk-Size"]= QByteArray::number(quint64(chunkSize()));
|
||||
headers["Content-Type"] = "application/octet-stream";
|
||||
headers["X-OC-Mtime"] = QByteArray::number(qint64(_item->_modtime));
|
||||
|
||||
@@ -509,291 +535,20 @@ void PropagateUploadFile::startNextChunk()
|
||||
}
|
||||
|
||||
if (!_item->_etag.isEmpty() && _item->_etag != "empty_etag"
|
||||
&& _item->_instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
|
||||
&& _item->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE
|
||||
&& !_deleteExisting
|
||||
) {
|
||||
&& _item->_instruction != CSYNC_INSTRUCTION_NEW // On new files never send a If-Match
|
||||
&& _item->_instruction != CSYNC_INSTRUCTION_TYPE_CHANGE
|
||||
&& !_deleteExisting
|
||||
) {
|
||||
// We add quotes because the owncloud server always adds quotes around the etag, and
|
||||
// csync_owncloud.c's owncloud_file_id always strips the quotes.
|
||||
headers["If-Match"] = '"' + _item->_etag + '"';
|
||||
}
|
||||
|
||||
QString path = _item->_file;
|
||||
|
||||
UploadDevice *device = new UploadDevice(&_propagator->_bandwidthManager);
|
||||
qint64 chunkStart = 0;
|
||||
qint64 currentChunkSize = fileSize;
|
||||
bool isFinalChunk = false;
|
||||
if (_chunkCount > 1) {
|
||||
int sendingChunk = (_currentChunk + _startChunk) % _chunkCount;
|
||||
// XOR with chunk size to make sure everything goes well if chunk size changes between runs
|
||||
uint transid = _transferId ^ chunkSize();
|
||||
qDebug() << "Upload chunk" << sendingChunk << "of" << _chunkCount << "transferid(remote)=" << transid;
|
||||
path += QString("-chunking-%1-%2-%3").arg(transid).arg(_chunkCount).arg(sendingChunk);
|
||||
|
||||
headers["OC-Chunked"] = "1";
|
||||
|
||||
chunkStart = chunkSize() * quint64(sendingChunk);
|
||||
currentChunkSize = chunkSize();
|
||||
if (sendingChunk == _chunkCount - 1) { // last chunk
|
||||
currentChunkSize = (fileSize % chunkSize());
|
||||
if( currentChunkSize == 0 ) { // if the last chunk pretends to be 0, its actually the full chunk size.
|
||||
currentChunkSize = chunkSize();
|
||||
}
|
||||
isFinalChunk = true;
|
||||
}
|
||||
} else {
|
||||
// if there's only one chunk, it's the final one
|
||||
isFinalChunk = true;
|
||||
}
|
||||
|
||||
if (isFinalChunk && !_transmissionChecksumType.isEmpty()) {
|
||||
headers[checkSumHeaderC] = makeChecksumHeader(
|
||||
_transmissionChecksumType, _transmissionChecksum);
|
||||
}
|
||||
|
||||
const QString fileName = _propagator->getFilePath(_item->_file);
|
||||
if (! device->prepareAndOpen(fileName, chunkStart, currentChunkSize)) {
|
||||
qDebug() << "ERR: Could not prepare upload device: " << device->errorString();
|
||||
|
||||
// If the file is currently locked, we want to retry the sync
|
||||
// when it becomes available again.
|
||||
if (FileSystem::isFileLocked(fileName)) {
|
||||
emit _propagator->seenLockedFile(fileName);
|
||||
}
|
||||
|
||||
// Soft error because this is likely caused by the user modifying his files while syncing
|
||||
abortWithError( SyncFileItem::SoftError, device->errorString() );
|
||||
delete device;
|
||||
return;
|
||||
}
|
||||
|
||||
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
|
||||
PUTFileJob* job = new PUTFileJob(_propagator->account(), _propagator->_remoteFolder + path, device, headers, _currentChunk);
|
||||
_jobs.append(job);
|
||||
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotPutFinished()));
|
||||
connect(job, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(slotUploadProgress(qint64,qint64)));
|
||||
connect(job, SIGNAL(uploadProgress(qint64,qint64)), device, SLOT(slotJobUploadProgress(qint64,qint64)));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
job->start();
|
||||
_propagator->_activeJobList.append(this);
|
||||
_currentChunk++;
|
||||
|
||||
bool parallelChunkUpload = true;
|
||||
QByteArray env = qgetenv("OWNCLOUD_PARALLEL_CHUNK");
|
||||
if (!env.isEmpty()) {
|
||||
parallelChunkUpload = env != "false" && env != "0";
|
||||
} else {
|
||||
int versionNum = _propagator->account()->serverVersionInt();
|
||||
if (versionNum < 0x080003) {
|
||||
// Disable parallel chunk upload severs older than 8.0.3 to avoid too many
|
||||
// internal sever errors (#2743, #2938)
|
||||
parallelChunkUpload = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentChunk + _startChunk >= _chunkCount - 1) {
|
||||
// Don't do parallel upload of chunk if this might be the last chunk because the server cannot handle that
|
||||
// https://github.com/owncloud/core/issues/11106
|
||||
parallelChunkUpload = false;
|
||||
}
|
||||
|
||||
if (parallelChunkUpload && (_propagator->_activeJobList.count() < _propagator->maximumActiveJob())
|
||||
&& _currentChunk < _chunkCount ) {
|
||||
startNextChunk();
|
||||
}
|
||||
if (!parallelChunkUpload || _chunkCount - _currentChunk <= 0) {
|
||||
emit ready();
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotPutFinished()
|
||||
void PropagateUploadFileCommon::finalize()
|
||||
{
|
||||
PUTFileJob *job = qobject_cast<PUTFileJob *>(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);
|
||||
|
||||
if (_finished) {
|
||||
// We have sent the finished signal already. We don't need to handle any remaining jobs
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
||||
if (err == 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();
|
||||
if(checkForProblemsWithShared(_item->_httpErrorCode,
|
||||
tr("The file was edited locally but is part of a read only share. "
|
||||
"It is restored and your edit is in the conflict file."))) {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
if (_item->_httpErrorCode == 412) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
SyncFileItem::Status status = classifyError(err, _item->_httpErrorCode,
|
||||
&_propagator->_anotherSyncNeeded);
|
||||
abortWithError(status, errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
// The server needs some time to process the request and provide us with a poll URL
|
||||
if (_item->_httpErrorCode == 202) {
|
||||
_finished = true;
|
||||
QString path = QString::fromUtf8(job->reply()->rawHeader("OC-Finish-Poll"));
|
||||
if (path.isEmpty()) {
|
||||
done(SyncFileItem::NormalError, tr("Poll URL missing"));
|
||||
return;
|
||||
}
|
||||
startPollJob(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the file again post upload.
|
||||
// Two cases must be considered separately: If the upload is finished,
|
||||
// the file is on the server and has a changed ETag. In that case,
|
||||
// the etag has to be properly updated in the client journal, and because
|
||||
// of that we can bail out here with an error. But we can reschedule a
|
||||
// sync ASAP.
|
||||
// But if the upload is ongoing, because not all chunks were uploaded
|
||||
// yet, the upload can be stopped and an error can be displayed, because
|
||||
// the server hasn't registered the new file yet.
|
||||
QByteArray etag = getEtagFromReply(job->reply());
|
||||
bool finished = etag.length() > 0;
|
||||
|
||||
// Check if the file still exists
|
||||
const QString fullFilePath(_propagator->getFilePath(_item->_file));
|
||||
if( !FileSystem::fileExists(fullFilePath) ) {
|
||||
if (!finished) {
|
||||
abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync."));
|
||||
return;
|
||||
} else {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the file changed since discovery.
|
||||
if (! FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
if( !finished ) {
|
||||
abortWithError(SyncFileItem::SoftError, tr("Local file changed during sync."));
|
||||
// FIXME: the legacy code was retrying for a few seconds.
|
||||
// and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finished) {
|
||||
// Proceed to next chunk.
|
||||
if (_currentChunk >= _chunkCount) {
|
||||
if (!_jobs.empty()) {
|
||||
// just wait for the other job to finish.
|
||||
return;
|
||||
}
|
||||
_finished = true;
|
||||
done(SyncFileItem::NormalError, tr("The server did not acknowledge the last chunk. (No e-tag was present)"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Deletes an existing blacklist entry on successful chunk upload
|
||||
if (_item->_hasBlacklistEntry) {
|
||||
_propagator->_journal->wipeErrorBlacklistEntry(_item->_file);
|
||||
_item->_hasBlacklistEntry = false;
|
||||
}
|
||||
|
||||
SyncJournalDb::UploadInfo pi;
|
||||
pi._valid = true;
|
||||
auto currentChunk = job->_chunk;
|
||||
foreach (auto *job, _jobs) {
|
||||
// Take the minimum finished one
|
||||
if (auto putJob = qobject_cast<PUTFileJob*>(job)) {
|
||||
currentChunk = qMin(currentChunk, putJob->_chunk - 1);
|
||||
}
|
||||
}
|
||||
pi._chunk = (currentChunk + _startChunk + 1) % _chunkCount ; // next chunk to start with
|
||||
pi._transferid = _transferId;
|
||||
pi._modtime = Utility::qDateTimeFromTime_t(_item->_modtime);
|
||||
_propagator->_journal->setUploadInfo(_item->_file, pi);
|
||||
_propagator->_journal->commit("Upload info");
|
||||
startNextChunk();
|
||||
return;
|
||||
}
|
||||
|
||||
// the following code only happens after all chunks were uploaded.
|
||||
_finished = true;
|
||||
// the file id should only be empty for new files up- or downloaded
|
||||
QByteArray fid = job->reply()->rawHeader("OC-FileID");
|
||||
if( !fid.isEmpty() ) {
|
||||
if( !_item->_fileId.isEmpty() && _item->_fileId != fid ) {
|
||||
qDebug() << "WARN: File ID changed!" << _item->_fileId << fid;
|
||||
}
|
||||
_item->_fileId = fid;
|
||||
}
|
||||
|
||||
_item->_etag = etag;
|
||||
|
||||
_item->_responseTimeStamp = job->responseTimestamp();
|
||||
|
||||
if (job->reply()->rawHeader("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" << job->reply()->rawHeader("X-OC-MTime");
|
||||
// Well, the mtime was not set
|
||||
done(SyncFileItem::SoftError, "Server does not support X-OC-MTime");
|
||||
}
|
||||
|
||||
// performance logging
|
||||
_item->_requestDuration = _stopWatch.stop();
|
||||
qDebug() << "*==* duration UPLOAD" << _item->_size
|
||||
<< _stopWatch.durationOfLap(QLatin1String("ContentChecksum"))
|
||||
<< _stopWatch.durationOfLap(QLatin1String("TransmissionChecksum"))
|
||||
<< _item->_requestDuration;
|
||||
// The job might stay alive for the whole sync, release this tiny bit of memory.
|
||||
_stopWatch.reset();
|
||||
|
||||
finalize(*_item);
|
||||
}
|
||||
|
||||
void PropagateUploadFile::finalize(const SyncFileItem ©)
|
||||
{
|
||||
// Normally, copy == _item, but when it comes from the UpdateMTimeAndETagJob, we need to do
|
||||
// some updates
|
||||
_item->_etag = copy._etag;
|
||||
_item->_fileId = copy._fileId;
|
||||
|
||||
_item->_requestDuration = _duration.elapsed();
|
||||
|
||||
_finished = true;
|
||||
|
||||
if (!_propagator->_journal->setFileRecord(SyncJournalFileRecord(*_item, _propagator->getFilePath(_item->_file)))) {
|
||||
@@ -807,92 +562,5 @@ void PropagateUploadFile::finalize(const SyncFileItem ©)
|
||||
done(SyncFileItem::Success);
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotUploadProgress(qint64 sent, qint64 total)
|
||||
{
|
||||
// Completion is signaled with sent=0, total=0; avoid accidentally
|
||||
// resetting progress due to the sent being zero by ignoring it.
|
||||
// finishedSignal() is bound to be emitted soon anyway.
|
||||
// See https://bugreports.qt.io/browse/QTBUG-44782.
|
||||
if (sent == 0 && total == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int progressChunk = _currentChunk + _startChunk - 1;
|
||||
if (progressChunk >= _chunkCount)
|
||||
progressChunk = _currentChunk - 1;
|
||||
|
||||
// amount is the number of bytes already sent by all the other chunks that were sent
|
||||
// not including this one.
|
||||
// FIXME: this assumes all chunks have the same size, which is true only if the last chunk
|
||||
// has not been finished (which should not happen because the last chunk is sent sequentially)
|
||||
quint64 amount = progressChunk * chunkSize();
|
||||
|
||||
sender()->setProperty("byteWritten", sent);
|
||||
if (_jobs.count() > 1) {
|
||||
amount -= (_jobs.count() -1) * chunkSize();
|
||||
foreach (QObject *j, _jobs) {
|
||||
amount += j->property("byteWritten").toULongLong();
|
||||
}
|
||||
} else {
|
||||
// sender() is the only current job, no need to look at the byteWritten properties
|
||||
amount += sent;
|
||||
}
|
||||
emit progress(*_item, amount);
|
||||
}
|
||||
|
||||
void PropagateUploadFile::startPollJob(const QString& path)
|
||||
{
|
||||
PollJob* job = new PollJob(_propagator->account(), path, _item,
|
||||
_propagator->_journal, _propagator->_localDir, this);
|
||||
connect(job, SIGNAL(finishedSignal()), SLOT(slotPollFinished()));
|
||||
SyncJournalDb::PollInfo info;
|
||||
info._file = _item->_file;
|
||||
info._url = path;
|
||||
info._modtime = _item->_modtime;
|
||||
_propagator->_journal->setPollInfo(info);
|
||||
_propagator->_journal->commit("add poll info");
|
||||
_propagator->_activeJobList.append(this);
|
||||
job->start();
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotPollFinished()
|
||||
{
|
||||
PollJob *job = qobject_cast<PollJob *>(sender());
|
||||
Q_ASSERT(job);
|
||||
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
|
||||
if (job->_item->_status != SyncFileItem::Success) {
|
||||
_finished = true;
|
||||
done(job->_item->_status, job->_item->_errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
finalize(*job->_item);
|
||||
}
|
||||
|
||||
void PropagateUploadFile::slotJobDestroyed(QObject* job)
|
||||
{
|
||||
_jobs.erase(std::remove(_jobs.begin(), _jobs.end(), job) , _jobs.end());
|
||||
}
|
||||
|
||||
void PropagateUploadFile::abort()
|
||||
{
|
||||
foreach(auto *job, _jobs) {
|
||||
if (job->reply()) {
|
||||
qDebug() << Q_FUNC_INFO << job << this->_item->_file;
|
||||
job->reply()->abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used whenever there is an error occuring and jobs might be in progress
|
||||
void PropagateUploadFile::abortWithError(SyncFileItem::Status status, const QString &error)
|
||||
{
|
||||
_finished = true;
|
||||
abort();
|
||||
done(status, error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+138
-38
@@ -89,12 +89,17 @@ private:
|
||||
QScopedPointer<QIODevice> _device;
|
||||
QMap<QByteArray, QByteArray> _headers;
|
||||
QString _errorString;
|
||||
QUrl _url;
|
||||
|
||||
public:
|
||||
// Takes ownership of the device
|
||||
explicit PUTFileJob(AccountPtr account, const QString& path, QIODevice *device,
|
||||
const QMap<QByteArray, QByteArray> &headers, int chunk, QObject* parent = 0)
|
||||
: AbstractNetworkJob(account, path, parent), _device(device), _headers(headers), _chunk(chunk) {}
|
||||
explicit PUTFileJob(AccountPtr account, const QUrl& url, QIODevice *device,
|
||||
const QMap<QByteArray, QByteArray> &headers, int chunk, QObject* parent = 0)
|
||||
: AbstractNetworkJob(account, QString(), parent), _device(device), _headers(headers)
|
||||
, _url(url), _chunk(chunk) {}
|
||||
~PUTFileJob();
|
||||
|
||||
int _chunk;
|
||||
@@ -155,10 +160,90 @@ signals:
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief The PropagateUploadFile class
|
||||
* @brief The PropagateUploadFileCommon class is the code common between all chunking algorithms
|
||||
* @ingroup libsync
|
||||
*
|
||||
* State Machine:
|
||||
*
|
||||
* +---> start() --> (delete job) -------+
|
||||
* | |
|
||||
* +--> slotComputeContentChecksum() <---+
|
||||
* |
|
||||
* v
|
||||
* slotComputeTransmissionChecksum()
|
||||
* |
|
||||
* v
|
||||
* slotStartUpload() -> doStartUpload()
|
||||
* .
|
||||
* .
|
||||
* v
|
||||
* finalize() or abortWithError() or startPollJob()
|
||||
*/
|
||||
class PropagateUploadFile : public PropagateItemJob {
|
||||
class PropagateUploadFileCommon : public PropagateItemJob {
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
QElapsedTimer _duration;
|
||||
QVector<AbstractNetworkJob*> _jobs; /// network jobs that are currently in transit
|
||||
bool _finished; /// Tells that all the jobs have been finished
|
||||
bool _deleteExisting;
|
||||
|
||||
// measure the performance of checksum calc and upload
|
||||
Utility::StopWatch _stopWatch;
|
||||
|
||||
QByteArray _transmissionChecksum;
|
||||
QByteArray _transmissionChecksumType;
|
||||
|
||||
|
||||
public:
|
||||
PropagateUploadFileCommon(OwncloudPropagator* propagator,const SyncFileItemPtr& item)
|
||||
: PropagateItemJob(propagator, item), _finished(false), _deleteExisting(false) {}
|
||||
|
||||
/**
|
||||
* Whether an existing entity with the same name may be deleted before
|
||||
* the upload.
|
||||
*
|
||||
* Default: false.
|
||||
*/
|
||||
void setDeleteExisting(bool enabled);
|
||||
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
|
||||
bool isLikelyFinishedQuickly() Q_DECL_OVERRIDE { return _item->_size < 100*1024; }
|
||||
|
||||
private slots:
|
||||
void slotComputeContentChecksum();
|
||||
// Content checksum computed, compute the transmission checksum
|
||||
void slotComputeTransmissionChecksum(const QByteArray& contentChecksumType, const QByteArray& contentChecksum);
|
||||
// transmission checksum computed, prepare the upload
|
||||
void slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum);
|
||||
public:
|
||||
virtual void doStartUpload() = 0;
|
||||
|
||||
void startPollJob(const QString& path);
|
||||
void finalize();
|
||||
void abortWithError(SyncFileItem::Status status, const QString &error);
|
||||
|
||||
public slots:
|
||||
void abort() Q_DECL_OVERRIDE;
|
||||
void slotJobDestroyed(QObject *job);
|
||||
|
||||
private slots:
|
||||
void slotPollFinished();
|
||||
|
||||
protected:
|
||||
// Bases headers that need to be sent with every chunk
|
||||
QMap<QByteArray, QByteArray> headers();
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @ingroup libsync
|
||||
*
|
||||
* Propagation job, impementing the old chunking agorithm
|
||||
*
|
||||
*/
|
||||
class PropagateUploadFileV1 : public PropagateUploadFileCommon {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
@@ -176,51 +261,66 @@ private:
|
||||
int _currentChunk;
|
||||
int _chunkCount; /// Total number of chunks for this file
|
||||
int _transferId; /// transfer id (part of the url)
|
||||
QElapsedTimer _duration;
|
||||
QVector<AbstractNetworkJob*> _jobs; /// network jobs that are currently in transit
|
||||
bool _finished; // Tells that all the jobs have been finished
|
||||
|
||||
// measure the performance of checksum calc and upload
|
||||
Utility::StopWatch _stopWatch;
|
||||
|
||||
QByteArray _transmissionChecksum;
|
||||
QByteArray _transmissionChecksumType;
|
||||
|
||||
bool _deleteExisting;
|
||||
|
||||
quint64 chunkSize() const { return _propagator->chunkSize(); }
|
||||
|
||||
|
||||
public:
|
||||
PropagateUploadFile(OwncloudPropagator* propagator,const SyncFileItemPtr& item)
|
||||
: PropagateItemJob(propagator, item), _startChunk(0), _currentChunk(0), _chunkCount(0), _transferId(0), _finished(false), _deleteExisting(false) {}
|
||||
void start() Q_DECL_OVERRIDE;
|
||||
PropagateUploadFileV1(OwncloudPropagator* propagator,const SyncFileItemPtr& item) :
|
||||
PropagateUploadFileCommon(propagator,item) {}
|
||||
|
||||
bool isLikelyFinishedQuickly() Q_DECL_OVERRIDE { return _item->_size < 100*1024; }
|
||||
|
||||
/**
|
||||
* Whether an existing entity with the same name may be deleted before
|
||||
* the upload.
|
||||
*
|
||||
* Default: false.
|
||||
*/
|
||||
void setDeleteExisting(bool enabled);
|
||||
void doStartUpload() Q_DECL_OVERRIDE;
|
||||
|
||||
private slots:
|
||||
void slotPutFinished();
|
||||
void slotPollFinished();
|
||||
void slotUploadProgress(qint64,qint64);
|
||||
void abort() Q_DECL_OVERRIDE;
|
||||
void startNextChunk();
|
||||
void finalize(const SyncFileItem&);
|
||||
void slotJobDestroyed(QObject *job);
|
||||
void slotStartUpload(const QByteArray& transmissionChecksumType, const QByteArray& transmissionChecksum);
|
||||
void slotComputeTransmissionChecksum(const QByteArray& contentChecksumType, const QByteArray& contentChecksum);
|
||||
void slotComputeContentChecksum();
|
||||
|
||||
private:
|
||||
void startPollJob(const QString& path);
|
||||
void abortWithError(SyncFileItem::Status status, const QString &error);
|
||||
void slotPutFinished();
|
||||
void slotUploadProgress(qint64,qint64);
|
||||
};
|
||||
|
||||
/**
|
||||
* @ingroup libsync
|
||||
*
|
||||
* Propagation job, impementing the new chunking agorithm
|
||||
*
|
||||
*/
|
||||
class PropagateUploadFileNG : public PropagateUploadFileCommon {
|
||||
Q_OBJECT
|
||||
private:
|
||||
quint64 _sent; /// amount of data (bytes) that was already sent
|
||||
uint _transferId; /// transfer id (part of the url)
|
||||
int _currentChunk; /// Id of the next chunk that will be sent
|
||||
bool _removeJobError; /// If not null, there was an error removing the job
|
||||
|
||||
// Map chunk number with its size from the PROPFIND on resume.
|
||||
// (Only used from slotPropfindIterate/slotPropfindFinished because the LsColJob use signals to report data.)
|
||||
QMap<int, quint64> _serverChunks;
|
||||
|
||||
quint64 chunkSize() const { return _propagator->chunkSize(); }
|
||||
/**
|
||||
* Return the URL of a chunk.
|
||||
* If chunk == -1, returns the URL of the parent folder containing the chunks
|
||||
*/
|
||||
QUrl chunkUrl(int chunk = -1);
|
||||
|
||||
public:
|
||||
PropagateUploadFileNG(OwncloudPropagator* propagator,const SyncFileItemPtr& item) :
|
||||
PropagateUploadFileCommon(propagator,item) {}
|
||||
|
||||
void doStartUpload() Q_DECL_OVERRIDE;
|
||||
private:
|
||||
void startNewUpload();
|
||||
void startNextChunk();
|
||||
private slots:
|
||||
void slotPropfindFinished();
|
||||
void slotPropfindFinishedWithError();
|
||||
void slotPropfindIterate(const QString &name, const QMap<QString,QString> &properties);
|
||||
void slotDeleteJobFinished();
|
||||
void slotMkColFinished(QNetworkReply::NetworkError);
|
||||
void slotPutFinished();
|
||||
void slotMoveJobFinished();
|
||||
void slotUploadProgress(qint64,qint64);
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "propagateupload.h"
|
||||
#include "owncloudpropagator_p.h"
|
||||
#include "networkjobs.h"
|
||||
#include "account.h"
|
||||
#include "syncjournaldb.h"
|
||||
#include "syncjournalfilerecord.h"
|
||||
#include "utility.h"
|
||||
#include "filesystem.h"
|
||||
#include "propagatorjobs.h"
|
||||
#include "syncengine.h"
|
||||
#include "propagateremotemove.h"
|
||||
#include "propagateremotedelete.h"
|
||||
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
QUrl PropagateUploadFileNG::chunkUrl(int chunk)
|
||||
{
|
||||
QString path = QLatin1String("remote.php/dav/uploads/")
|
||||
+ _propagator->account()->davUser()
|
||||
+ QLatin1Char('/') + QString::number(_transferId);
|
||||
if (chunk >= 0) {
|
||||
path += QLatin1Char('/') + QString::number(chunk);
|
||||
}
|
||||
return Utility::concatUrlPath(_propagator->account()->url(), path);
|
||||
}
|
||||
|
||||
/*
|
||||
State machine:
|
||||
|
||||
*----> doStartUpload()
|
||||
Check the db: is there an entry?
|
||||
/ \
|
||||
no yes
|
||||
/ \
|
||||
/ PROPFIND
|
||||
startNewUpload() <-+ +----------------------------\
|
||||
| | | \
|
||||
MKCOL + slotPropfindFinishedWithError() slotPropfindFinished()
|
||||
| Is there stale files to remove?
|
||||
slotMkColFinished() | |
|
||||
| no yes
|
||||
| | |
|
||||
| | DeleteJob
|
||||
| | |
|
||||
+-----+<------------------------------------------------------+<--- slotDeleteJobFinished()
|
||||
|
|
||||
+----> startNextChunk() ---finished? --+
|
||||
^ | |
|
||||
+---------------+ |
|
||||
|
|
||||
+----------------------------------------+
|
||||
|
|
||||
+-> MOVE ------> moveJobFinished() ---> finalize()
|
||||
|
||||
|
||||
*/
|
||||
|
||||
void PropagateUploadFileNG::doStartUpload()
|
||||
{
|
||||
_duration.start();
|
||||
_propagator->_activeJobList.append(this);
|
||||
|
||||
const SyncJournalDb::UploadInfo progressInfo = _propagator->_journal->getUploadInfo(_item->_file);
|
||||
if (progressInfo._valid && Utility::qDateTimeToTime_t(progressInfo._modtime) == _item->_modtime ) {
|
||||
_transferId = progressInfo._transferid;
|
||||
auto url = chunkUrl();
|
||||
auto job = new LsColJob(_propagator->account(), url, this);
|
||||
_jobs.append(job);
|
||||
job->setProperties(QList<QByteArray>() << "resourcetype" << "getcontentlength");
|
||||
connect(job, SIGNAL(finishedWithoutError()), this, SLOT(slotPropfindFinished()));
|
||||
connect(job, SIGNAL(finishedWithError(QNetworkReply*)),
|
||||
this, SLOT(slotPropfindFinishedWithError()));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
connect(job, SIGNAL(directoryListingIterated(QString,QMap<QString,QString>)),
|
||||
this, SLOT(slotPropfindIterate(QString,QMap<QString,QString>)));
|
||||
job->start();
|
||||
return;
|
||||
}
|
||||
|
||||
startNewUpload();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotPropfindIterate(const QString &name, const QMap<QString,QString> &properties)
|
||||
{
|
||||
if (name == chunkUrl().path()) {
|
||||
return; // skip the info about the path itself
|
||||
}
|
||||
bool ok = false;
|
||||
auto chunkId = name.mid(name.lastIndexOf('/')+1).toUInt(&ok);
|
||||
if (ok) {
|
||||
_serverChunks[chunkId] = properties["getcontentlength"].toULongLong();
|
||||
}
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotPropfindFinished()
|
||||
{
|
||||
auto job = qobject_cast<LsColJob *>(sender());
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
|
||||
_currentChunk = 0;
|
||||
_sent = 0;
|
||||
while (_serverChunks.contains(_currentChunk)) {
|
||||
_sent += _serverChunks[_currentChunk];
|
||||
_serverChunks.remove(_currentChunk);
|
||||
++_currentChunk;
|
||||
}
|
||||
|
||||
if (_sent > _item->_size) {
|
||||
// Normally this can't happen because the size is xor'ed with the transfer id, and it is
|
||||
// therefore impossible that there is more data on the server than on the file.
|
||||
qWarning() << "Inconsistency while resuming " << _item->_file
|
||||
<< ": the size on the server (" << _sent << ") is bigger than the size of the file ("
|
||||
<< _item->_size << ")";
|
||||
startNewUpload();
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Resuming "<< _item->_file << " from chunk " << _currentChunk << "; sent ="<< _sent;
|
||||
|
||||
if (!_serverChunks.isEmpty()) {
|
||||
qDebug() << "To Delete" << _serverChunks;
|
||||
_propagator->_activeJobList.append(this);
|
||||
_removeJobError = false;
|
||||
|
||||
// Make sure that if there is a "hole" and then a few more chunks, on the server
|
||||
// we should remove the later chunks. Otherwise when we do dynamic chunk sizing, we may end up
|
||||
// with corruptions if there are too many chunks, or if we abort and there are still stale chunks.
|
||||
for (auto it = _serverChunks.begin(); it != _serverChunks.end(); ++it) {
|
||||
auto job = new DeleteJob(_propagator->account(), Utility::concatUrlPath(chunkUrl(), QString::number(it.key())), this);
|
||||
QObject::connect(job, SIGNAL(finishedSignal()), this, SLOT(slotDeleteJobFinished()));
|
||||
_jobs.append(job);
|
||||
job->start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
startNextChunk();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotPropfindFinishedWithError()
|
||||
{
|
||||
auto job = qobject_cast<LsColJob *>(sender());
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
auto httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
auto status = classifyError(err, httpErrorCode, &_propagator->_anotherSyncNeeded);
|
||||
if (status == SyncFileItem::FatalError) {
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
QString errorString = errorMessage(job->reply()->errorString(), job->reply()->readAll());
|
||||
abortWithError(status, errorString);
|
||||
return;
|
||||
}
|
||||
startNewUpload();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotDeleteJobFinished()
|
||||
{
|
||||
auto job = qobject_cast<DeleteJob *>(sender());
|
||||
Q_ASSERT(job);
|
||||
_jobs.remove(_jobs.indexOf(job));
|
||||
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
if (err != QNetworkReply::NoError && err != QNetworkReply::ContentNotFoundError) {
|
||||
const int httpStatus = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
SyncFileItem::Status status = classifyError(err, httpStatus);
|
||||
if (status == SyncFileItem::FatalError) {
|
||||
abortWithError(status, job->errorString());
|
||||
return;
|
||||
} else {
|
||||
qWarning() << "DeleteJob errored out" << job->errorString() << job->reply()->url();
|
||||
_removeJobError = true;
|
||||
// Let the other jobs finish
|
||||
}
|
||||
}
|
||||
|
||||
if (_jobs.isEmpty()) {
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
if (_removeJobError) {
|
||||
// There was an error removing some files, just start over
|
||||
startNewUpload();
|
||||
} else {
|
||||
startNextChunk();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void PropagateUploadFileNG::startNewUpload()
|
||||
{
|
||||
Q_ASSERT(_propagator->_activeJobList.count(this) == 1);
|
||||
_transferId = qrand() ^ _item->_modtime ^ (_item->_size << 16) ^ qHash(_item->_file);
|
||||
_sent = 0;
|
||||
_currentChunk = 0;
|
||||
|
||||
emit progress(*_item, 0);
|
||||
|
||||
SyncJournalDb::UploadInfo pi;
|
||||
pi._valid = true;
|
||||
pi._transferid = _transferId;
|
||||
pi._modtime = Utility::qDateTimeFromTime_t(_item->_modtime);
|
||||
_propagator->_journal->setUploadInfo(_item->_file, pi);
|
||||
_propagator->_journal->commit("Upload info");
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
headers["OC-Total-Length"] = QByteArray::number(_item->_size);
|
||||
auto job = new MkColJob(_propagator->account(), chunkUrl(), headers, this);
|
||||
|
||||
connect(job, SIGNAL(finished(QNetworkReply::NetworkError)),
|
||||
this, SLOT(slotMkColFinished(QNetworkReply::NetworkError)));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
job->start();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotMkColFinished(QNetworkReply::NetworkError)
|
||||
{
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
auto job = qobject_cast<MkColJob *>(sender());
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (err != QNetworkReply::NoError || _item->_httpErrorCode != 201) {
|
||||
SyncFileItem::Status status = classifyError(err, _item->_httpErrorCode,
|
||||
&_propagator->_anotherSyncNeeded);
|
||||
QString errorString = errorMessage(job->reply()->errorString(), job->reply()->readAll());
|
||||
if (job->reply()->hasRawHeader("OC-ErrorString")) {
|
||||
errorString = job->reply()->rawHeader("OC-ErrorString");
|
||||
}
|
||||
abortWithError(status, errorString);
|
||||
return;
|
||||
}
|
||||
startNextChunk();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::startNextChunk()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
|
||||
return;
|
||||
|
||||
quint64 fileSize = _item->_size;
|
||||
Q_ASSERT(fileSize >= _sent);
|
||||
quint64 currentChunkSize = qMin(chunkSize(), fileSize - _sent);
|
||||
|
||||
if (currentChunkSize == 0) {
|
||||
Q_ASSERT(_jobs.isEmpty()); // There should be no running job anymore
|
||||
_finished = true;
|
||||
// Finish with a MOVE
|
||||
QString destination = _propagator->account()->url().path()
|
||||
+ QLatin1String("/remote.php/dav/files/") + _propagator->account()->davUser()
|
||||
+ _propagator->_remoteFolder + _item->_file;
|
||||
|
||||
auto headers = PropagateUploadFileCommon::headers();
|
||||
|
||||
// "If-Match applies to the source, but we are interested in comparing the etag of the destination
|
||||
auto ifMatch = headers.take("If-Match");
|
||||
if (!ifMatch.isEmpty()) {
|
||||
headers["If"] = "<" + destination.toUtf8() + "> ([" + ifMatch + "])";
|
||||
}
|
||||
if (!_transmissionChecksumType.isEmpty()) {
|
||||
headers[checkSumHeaderC] = makeChecksumHeader(
|
||||
_transmissionChecksumType, _transmissionChecksum);
|
||||
}
|
||||
|
||||
auto job = new MoveJob(_propagator->account(), Utility::concatUrlPath(chunkUrl(), "/.file"),
|
||||
destination, headers, this);
|
||||
_jobs.append(job);
|
||||
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotMoveJobFinished()));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
_propagator->_activeJobList.append(this);
|
||||
job->start();
|
||||
return;
|
||||
}
|
||||
|
||||
auto device = new UploadDevice(&_propagator->_bandwidthManager);
|
||||
const QString fileName = _propagator->getFilePath(_item->_file);
|
||||
|
||||
if (! device->prepareAndOpen(fileName, _sent, currentChunkSize)) {
|
||||
qDebug() << "ERR: Could not prepare upload device: " << device->errorString();
|
||||
|
||||
// If the file is currently locked, we want to retry the sync
|
||||
// when it becomes available again.
|
||||
if (FileSystem::isFileLocked(fileName)) {
|
||||
emit _propagator->seenLockedFile(fileName);
|
||||
}
|
||||
// Soft error because this is likely caused by the user modifying his files while syncing
|
||||
abortWithError( SyncFileItem::SoftError, device->errorString() );
|
||||
return;
|
||||
}
|
||||
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
headers["OC-Chunk-Offset"] = QByteArray::number(_sent);
|
||||
|
||||
_sent += currentChunkSize;
|
||||
QUrl url = chunkUrl(_currentChunk);
|
||||
|
||||
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
|
||||
PUTFileJob* job = new PUTFileJob(_propagator->account(), url, device, headers, _currentChunk);
|
||||
_jobs.append(job);
|
||||
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotPutFinished()));
|
||||
connect(job, SIGNAL(uploadProgress(qint64,qint64)),
|
||||
this, SLOT(slotUploadProgress(qint64,qint64)));
|
||||
connect(job, SIGNAL(uploadProgress(qint64,qint64)),
|
||||
device, SLOT(slotJobUploadProgress(qint64,qint64)));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
job->start();
|
||||
_propagator->_activeJobList.append(this);
|
||||
_currentChunk++;
|
||||
|
||||
// FIXME! parallel chunk?
|
||||
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotPutFinished()
|
||||
{
|
||||
PUTFileJob *job = qobject_cast<PUTFileJob *>(sender());
|
||||
Q_ASSERT(job);
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
|
||||
qDebug() << 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);
|
||||
|
||||
if (_finished) {
|
||||
// We have sent the finished signal already. We don't need to handle any remaining jobs
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
||||
if (err == QNetworkReply::OperationCanceledError && job->reply()->property("owncloud-should-soft-cancel").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");
|
||||
}
|
||||
|
||||
// FIXME! can this happen for the chunks?
|
||||
if (_item->_httpErrorCode == 412) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
SyncFileItem::Status status = classifyError(err, _item->_httpErrorCode,
|
||||
&_propagator->_anotherSyncNeeded);
|
||||
abortWithError(status, errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
Q_ASSERT(_sent <= _item->_size);
|
||||
bool finished = _sent == _item->_size;
|
||||
|
||||
// Check if the file still exists
|
||||
const QString fullFilePath(_propagator->getFilePath(_item->_file));
|
||||
if( !FileSystem::fileExists(fullFilePath) ) {
|
||||
if (!finished) {
|
||||
abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync."));
|
||||
return;
|
||||
} else {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the file changed since discovery.
|
||||
if (! FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
if( !finished ) {
|
||||
abortWithError(SyncFileItem::SoftError, tr("Local file changed during sync."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finished) {
|
||||
// Deletes an existing blacklist entry on successful chunk upload
|
||||
if (_item->_hasBlacklistEntry) {
|
||||
_propagator->_journal->wipeErrorBlacklistEntry(_item->_file);
|
||||
_item->_hasBlacklistEntry = false;
|
||||
}
|
||||
}
|
||||
startNextChunk();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotMoveJobFinished()
|
||||
{
|
||||
_propagator->_activeJobList.removeOne(this);
|
||||
auto job = qobject_cast<MoveJob *>(sender());
|
||||
slotJobDestroyed(job); // remove it from the _jobs list
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (err != QNetworkReply::NoError) {
|
||||
SyncFileItem::Status status = classifyError(err, _item->_httpErrorCode,
|
||||
&_propagator->_anotherSyncNeeded);
|
||||
QString errorString = errorMessage(job->errorString(), job->reply()->readAll());
|
||||
abortWithError(status, errorString);
|
||||
return;
|
||||
}
|
||||
if (_item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) {
|
||||
abortWithError(SyncFileItem::NormalError, tr("Unexpected return code from server (%1)").arg(_item->_httpErrorCode));
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray fid = job->reply()->rawHeader("OC-FileID");
|
||||
if(fid.isEmpty()) {
|
||||
qWarning() << "Server did not return a OC-FileID" << _item->_file;
|
||||
abortWithError(SyncFileItem::NormalError, tr("Missing File ID from server"));
|
||||
return;
|
||||
} else {
|
||||
// the old file id should only be empty for new files uploaded
|
||||
if( !_item->_fileId.isEmpty() && _item->_fileId != fid ) {
|
||||
qDebug() << "WARN: File ID changed!" << _item->_fileId << fid;
|
||||
}
|
||||
_item->_fileId = fid;
|
||||
}
|
||||
|
||||
_item->_etag = getEtagFromReply(job->reply());;
|
||||
if (_item->_etag.isEmpty()) {
|
||||
qWarning() << "Server did not return an ETAG" << _item->_file;
|
||||
abortWithError(SyncFileItem::NormalError, tr("Missing ETag from server"));
|
||||
return;
|
||||
}
|
||||
_item->_responseTimeStamp = job->responseTimestamp();
|
||||
|
||||
// performance logging
|
||||
_item->_requestDuration = _stopWatch.stop();
|
||||
qDebug() << "*==* duration UPLOAD" << _item->_size
|
||||
<< _stopWatch.durationOfLap(QLatin1String("ContentChecksum"))
|
||||
<< _stopWatch.durationOfLap(QLatin1String("TransmissionChecksum"))
|
||||
<< _item->_requestDuration;
|
||||
// The job might stay alive for the whole sync, release this tiny bit of memory.
|
||||
_stopWatch.reset();
|
||||
finalize();
|
||||
}
|
||||
|
||||
void PropagateUploadFileNG::slotUploadProgress(qint64 sent, qint64 total)
|
||||
{
|
||||
// Completion is signaled with sent=0, total=0; avoid accidentally
|
||||
// resetting progress due to the sent being zero by ignoring it.
|
||||
// finishedSignal() is bound to be emitted soon anyway.
|
||||
// See https://bugreports.qt.io/browse/QTBUG-44782.
|
||||
if (sent == 0 && total == 0) {
|
||||
return;
|
||||
}
|
||||
emit progress(*_item, _sent + sent - total);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* Copyright (C) by Olivier Goffart <ogoffart@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
#include "propagateupload.h"
|
||||
#include "owncloudpropagator_p.h"
|
||||
#include "networkjobs.h"
|
||||
#include "account.h"
|
||||
#include "syncjournaldb.h"
|
||||
#include "syncjournalfilerecord.h"
|
||||
#include "utility.h"
|
||||
#include "filesystem.h"
|
||||
#include "propagatorjobs.h"
|
||||
#include "checksums.h"
|
||||
#include "syncengine.h"
|
||||
#include "propagateremotedelete.h"
|
||||
|
||||
#include <json.h>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace OCC {
|
||||
void PropagateUploadFileV1::doStartUpload()
|
||||
{
|
||||
_chunkCount = std::ceil(_item->_size / double(chunkSize()));
|
||||
_startChunk = 0;
|
||||
_transferId = qrand() ^ _item->_modtime ^ (_item->_size << 16);
|
||||
|
||||
const SyncJournalDb::UploadInfo progressInfo = _propagator->_journal->getUploadInfo(_item->_file);
|
||||
|
||||
if (progressInfo._valid && Utility::qDateTimeToTime_t(progressInfo._modtime) == _item->_modtime ) {
|
||||
_startChunk = progressInfo._chunk;
|
||||
_transferId = progressInfo._transferid;
|
||||
qDebug() << Q_FUNC_INFO << _item->_file << ": Resuming from chunk " << _startChunk;
|
||||
}
|
||||
|
||||
_currentChunk = 0;
|
||||
_duration.start();
|
||||
|
||||
emit progress(*_item, 0);
|
||||
startNextChunk();
|
||||
}
|
||||
|
||||
void PropagateUploadFileV1::startNextChunk()
|
||||
{
|
||||
if (_propagator->_abortRequested.fetchAndAddRelaxed(0))
|
||||
return;
|
||||
|
||||
if (! _jobs.isEmpty() && _currentChunk + _startChunk >= _chunkCount - 1) {
|
||||
// Don't do parallel upload of chunk if this might be the last chunk because the server cannot handle that
|
||||
// https://github.com/owncloud/core/issues/11106
|
||||
// We return now and when the _jobs are finished we will proceed with the last chunk
|
||||
// NOTE: Some other parts of the code such as slotUploadProgress also assume that the last chunk
|
||||
// is sent last.
|
||||
return;
|
||||
}
|
||||
quint64 fileSize = _item->_size;
|
||||
auto headers = PropagateUploadFileCommon::headers();
|
||||
headers["OC-Total-Length"] = QByteArray::number(fileSize);
|
||||
headers["OC-Chunk-Size"]= QByteArray::number(quint64(chunkSize()));
|
||||
|
||||
QString path = _item->_file;
|
||||
|
||||
UploadDevice *device = new UploadDevice(&_propagator->_bandwidthManager);
|
||||
qint64 chunkStart = 0;
|
||||
qint64 currentChunkSize = fileSize;
|
||||
bool isFinalChunk = false;
|
||||
if (_chunkCount > 1) {
|
||||
int sendingChunk = (_currentChunk + _startChunk) % _chunkCount;
|
||||
// XOR with chunk size to make sure everything goes well if chunk size changes between runs
|
||||
uint transid = _transferId ^ chunkSize();
|
||||
qDebug() << "Upload chunk" << sendingChunk << "of" << _chunkCount << "transferid(remote)=" << transid;
|
||||
path += QString("-chunking-%1-%2-%3").arg(transid).arg(_chunkCount).arg(sendingChunk);
|
||||
|
||||
headers["OC-Chunked"] = "1";
|
||||
|
||||
chunkStart = chunkSize() * quint64(sendingChunk);
|
||||
currentChunkSize = chunkSize();
|
||||
if (sendingChunk == _chunkCount - 1) { // last chunk
|
||||
currentChunkSize = (fileSize % chunkSize());
|
||||
if( currentChunkSize == 0 ) { // if the last chunk pretends to be 0, its actually the full chunk size.
|
||||
currentChunkSize = chunkSize();
|
||||
}
|
||||
isFinalChunk = true;
|
||||
}
|
||||
} else {
|
||||
// if there's only one chunk, it's the final one
|
||||
isFinalChunk = true;
|
||||
}
|
||||
|
||||
if (isFinalChunk && !_transmissionChecksumType.isEmpty()) {
|
||||
headers[checkSumHeaderC] = makeChecksumHeader(
|
||||
_transmissionChecksumType, _transmissionChecksum);
|
||||
}
|
||||
|
||||
const QString fileName = _propagator->getFilePath(_item->_file);
|
||||
if (! device->prepareAndOpen(fileName, chunkStart, currentChunkSize)) {
|
||||
qDebug() << "ERR: Could not prepare upload device: " << device->errorString();
|
||||
|
||||
// If the file is currently locked, we want to retry the sync
|
||||
// when it becomes available again.
|
||||
if (FileSystem::isFileLocked(fileName)) {
|
||||
emit _propagator->seenLockedFile(fileName);
|
||||
}
|
||||
// Soft error because this is likely caused by the user modifying his files while syncing
|
||||
abortWithError( SyncFileItem::SoftError, device->errorString() );
|
||||
delete device;
|
||||
return;
|
||||
}
|
||||
|
||||
// job takes ownership of device via a QScopedPointer. Job deletes itself when finishing
|
||||
PUTFileJob* job = new PUTFileJob(_propagator->account(), _propagator->_remoteFolder + path, device, headers, _currentChunk);
|
||||
_jobs.append(job);
|
||||
connect(job, SIGNAL(finishedSignal()), this, SLOT(slotPutFinished()));
|
||||
connect(job, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(slotUploadProgress(qint64,qint64)));
|
||||
connect(job, SIGNAL(uploadProgress(qint64,qint64)), device, SLOT(slotJobUploadProgress(qint64,qint64)));
|
||||
connect(job, SIGNAL(destroyed(QObject*)), this, SLOT(slotJobDestroyed(QObject*)));
|
||||
job->start();
|
||||
_propagator->_activeJobList.append(this);
|
||||
_currentChunk++;
|
||||
|
||||
bool parallelChunkUpload = true;
|
||||
QByteArray env = qgetenv("OWNCLOUD_PARALLEL_CHUNK");
|
||||
if (!env.isEmpty()) {
|
||||
parallelChunkUpload = env != "false" && env != "0";
|
||||
} else {
|
||||
int versionNum = _propagator->account()->serverVersionInt();
|
||||
if (versionNum < 0x080003) {
|
||||
// Disable parallel chunk upload severs older than 8.0.3 to avoid too many
|
||||
// internal sever errors (#2743, #2938)
|
||||
parallelChunkUpload = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentChunk + _startChunk >= _chunkCount - 1) {
|
||||
// Don't do parallel upload of chunk if this might be the last chunk because the server cannot handle that
|
||||
// https://github.com/owncloud/core/issues/11106
|
||||
parallelChunkUpload = false;
|
||||
}
|
||||
|
||||
if (parallelChunkUpload && (_propagator->_activeJobList.count() < _propagator->maximumActiveJob())
|
||||
&& _currentChunk < _chunkCount ) {
|
||||
startNextChunk();
|
||||
}
|
||||
if (!parallelChunkUpload || _chunkCount - _currentChunk <= 0) {
|
||||
emit ready();
|
||||
}
|
||||
}
|
||||
|
||||
void PropagateUploadFileV1::slotPutFinished()
|
||||
{
|
||||
PUTFileJob *job = qobject_cast<PUTFileJob *>(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);
|
||||
|
||||
if (_finished) {
|
||||
// We have sent the finished signal already. We don't need to handle any remaining jobs
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkReply::NetworkError err = job->reply()->error();
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 4, 2)
|
||||
if (err == QNetworkReply::OperationCanceledError && job->reply()->property("owncloud-should-soft-cancel").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();
|
||||
if(checkForProblemsWithShared(_item->_httpErrorCode,
|
||||
tr("The file was edited locally but is part of a read only share. "
|
||||
"It is restored and your edit is in the conflict file."))) {
|
||||
return;
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
if (_item->_httpErrorCode == 412) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
SyncFileItem::Status status = classifyError(err, _item->_httpErrorCode,
|
||||
&_propagator->_anotherSyncNeeded);
|
||||
abortWithError(status, errorString);
|
||||
return;
|
||||
}
|
||||
|
||||
_item->_httpErrorCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
// The server needs some time to process the request and provide us with a poll URL
|
||||
if (_item->_httpErrorCode == 202) {
|
||||
_finished = true;
|
||||
QString path = QString::fromUtf8(job->reply()->rawHeader("OC-Finish-Poll"));
|
||||
if (path.isEmpty()) {
|
||||
done(SyncFileItem::NormalError, tr("Poll URL missing"));
|
||||
return;
|
||||
}
|
||||
startPollJob(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the file again post upload.
|
||||
// Two cases must be considered separately: If the upload is finished,
|
||||
// the file is on the server and has a changed ETag. In that case,
|
||||
// the etag has to be properly updated in the client journal, and because
|
||||
// of that we can bail out here with an error. But we can reschedule a
|
||||
// sync ASAP.
|
||||
// But if the upload is ongoing, because not all chunks were uploaded
|
||||
// yet, the upload can be stopped and an error can be displayed, because
|
||||
// the server hasn't registered the new file yet.
|
||||
QByteArray etag = getEtagFromReply(job->reply());
|
||||
bool finished = etag.length() > 0;
|
||||
|
||||
// Check if the file still exists
|
||||
const QString fullFilePath(_propagator->getFilePath(_item->_file));
|
||||
if( !FileSystem::fileExists(fullFilePath) ) {
|
||||
if (!finished) {
|
||||
abortWithError(SyncFileItem::SoftError, tr("The local file was removed during sync."));
|
||||
return;
|
||||
} else {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the file changed since discovery.
|
||||
if (! FileSystem::verifyFileUnchanged(fullFilePath, _item->_size, _item->_modtime)) {
|
||||
_propagator->_anotherSyncNeeded = true;
|
||||
if( !finished ) {
|
||||
abortWithError(SyncFileItem::SoftError, tr("Local file changed during sync."));
|
||||
// FIXME: the legacy code was retrying for a few seconds.
|
||||
// and also checking that after the last chunk, and removed the file in case of INSTRUCTION_NEW
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finished) {
|
||||
// Proceed to next chunk.
|
||||
if (_currentChunk >= _chunkCount) {
|
||||
if (!_jobs.empty()) {
|
||||
// just wait for the other job to finish.
|
||||
return;
|
||||
}
|
||||
_finished = true;
|
||||
done(SyncFileItem::NormalError, tr("The server did not acknowledge the last chunk. (No e-tag was present)"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Deletes an existing blacklist entry on successful chunk upload
|
||||
if (_item->_hasBlacklistEntry) {
|
||||
_propagator->_journal->wipeErrorBlacklistEntry(_item->_file);
|
||||
_item->_hasBlacklistEntry = false;
|
||||
}
|
||||
|
||||
SyncJournalDb::UploadInfo pi;
|
||||
pi._valid = true;
|
||||
auto currentChunk = job->_chunk;
|
||||
foreach (auto *job, _jobs) {
|
||||
// Take the minimum finished one
|
||||
if (auto putJob = qobject_cast<PUTFileJob*>(job)) {
|
||||
currentChunk = qMin(currentChunk, putJob->_chunk - 1);
|
||||
}
|
||||
}
|
||||
pi._chunk = (currentChunk + _startChunk + 1) % _chunkCount ; // next chunk to start with
|
||||
pi._transferid = _transferId;
|
||||
pi._modtime = Utility::qDateTimeFromTime_t(_item->_modtime);
|
||||
_propagator->_journal->setUploadInfo(_item->_file, pi);
|
||||
_propagator->_journal->commit("Upload info");
|
||||
startNextChunk();
|
||||
return;
|
||||
}
|
||||
|
||||
// the following code only happens after all chunks were uploaded.
|
||||
_finished = true;
|
||||
// the file id should only be empty for new files up- or downloaded
|
||||
QByteArray fid = job->reply()->rawHeader("OC-FileID");
|
||||
if( !fid.isEmpty() ) {
|
||||
if( !_item->_fileId.isEmpty() && _item->_fileId != fid ) {
|
||||
qDebug() << "WARN: File ID changed!" << _item->_fileId << fid;
|
||||
}
|
||||
_item->_fileId = fid;
|
||||
}
|
||||
|
||||
_item->_etag = etag;
|
||||
|
||||
_item->_responseTimeStamp = job->responseTimestamp();
|
||||
|
||||
if (job->reply()->rawHeader("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" << job->reply()->rawHeader("X-OC-MTime");
|
||||
// Well, the mtime was not set
|
||||
done(SyncFileItem::SoftError, "Server does not support X-OC-MTime");
|
||||
}
|
||||
|
||||
// performance logging
|
||||
_item->_requestDuration = _stopWatch.stop();
|
||||
qDebug() << "*==* duration UPLOAD" << _item->_size
|
||||
<< _stopWatch.durationOfLap(QLatin1String("ContentChecksum"))
|
||||
<< _stopWatch.durationOfLap(QLatin1String("TransmissionChecksum"))
|
||||
<< _item->_requestDuration;
|
||||
// The job might stay alive for the whole sync, release this tiny bit of memory.
|
||||
_stopWatch.reset();
|
||||
|
||||
finalize();
|
||||
}
|
||||
|
||||
|
||||
void PropagateUploadFileV1::slotUploadProgress(qint64 sent, qint64 total)
|
||||
{
|
||||
// Completion is signaled with sent=0, total=0; avoid accidentally
|
||||
// resetting progress due to the sent being zero by ignoring it.
|
||||
// finishedSignal() is bound to be emitted soon anyway.
|
||||
// See https://bugreports.qt.io/browse/QTBUG-44782.
|
||||
if (sent == 0 && total == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int progressChunk = _currentChunk + _startChunk - 1;
|
||||
if (progressChunk >= _chunkCount)
|
||||
progressChunk = _currentChunk - 1;
|
||||
|
||||
// amount is the number of bytes already sent by all the other chunks that were sent
|
||||
// not including this one.
|
||||
// FIXME: this assumes all chunks have the same size, which is true only if the last chunk
|
||||
// has not been finished (which should not happen because the last chunk is sent sequentially)
|
||||
quint64 amount = progressChunk * chunkSize();
|
||||
|
||||
sender()->setProperty("byteWritten", sent);
|
||||
if (_jobs.count() > 1) {
|
||||
amount -= (_jobs.count() -1) * chunkSize();
|
||||
foreach (QObject *j, _jobs) {
|
||||
amount += j->property("byteWritten").toULongLong();
|
||||
}
|
||||
} else {
|
||||
// sender() is the only current job, no need to look at the byteWritten properties
|
||||
amount += sent;
|
||||
}
|
||||
emit progress(*_item, amount);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,12 +57,11 @@ bool SyncEngine::s_anySyncRunning = false;
|
||||
qint64 SyncEngine::minimumFileAgeForUpload = 2000;
|
||||
|
||||
SyncEngine::SyncEngine(AccountPtr account, const QString& localPath,
|
||||
const QUrl& remoteURL, const QString& remotePath, OCC::SyncJournalDb* journal)
|
||||
const QString& remotePath, OCC::SyncJournalDb* journal)
|
||||
: _account(account)
|
||||
, _needsUpdate(false)
|
||||
, _syncRunning(false)
|
||||
, _localPath(localPath)
|
||||
, _remoteUrl(remoteURL)
|
||||
, _remotePath(remotePath)
|
||||
, _journal(journal)
|
||||
, _progressInfo(new ProgressInfo)
|
||||
@@ -83,18 +82,7 @@ SyncEngine::SyncEngine(AccountPtr account, const QString& localPath,
|
||||
// Everything in the SyncEngine expects a trailing slash for the localPath.
|
||||
Q_ASSERT(localPath.endsWith(QLatin1Char('/')));
|
||||
|
||||
// We need to reconstruct the url because the path needs to be fully decoded, as csync will re-encode the path:
|
||||
// Remember that csync will just append the filename to the path and pass it to the vio plugin.
|
||||
// csync_owncloud will then re-encode everything.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
|
||||
QString url_string = _remoteUrl.scheme() + QLatin1String("://") + _remoteUrl.authority(QUrl::EncodeDelimiters) + _remoteUrl.path(QUrl::FullyDecoded);
|
||||
#else
|
||||
// Qt4 was broken anyway as it did not encode the '#' as it should have done (it was actually a problem when parsing the path from QUrl::setPath
|
||||
QString url_string = _remoteUrl.toString();
|
||||
#endif
|
||||
url_string = Utility::toCSyncScheme(url_string);
|
||||
|
||||
csync_create(&_csync_ctx, localPath.toUtf8().data(), url_string.toUtf8().data());
|
||||
csync_create(&_csync_ctx, localPath.toUtf8().data());
|
||||
csync_init(_csync_ctx);
|
||||
_excludedFiles.reset(new ExcludedFiles(&_csync_ctx->excludes));
|
||||
_syncFileStatusTracker.reset(new SyncFileStatusTracker(this));
|
||||
@@ -334,6 +322,16 @@ 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;
|
||||
@@ -589,11 +587,15 @@ 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) {
|
||||
if (std::difftime(file->modtime, file->other.modtime) < 0) {
|
||||
auto difftime = std::difftime(file->modtime, file->other.modtime);
|
||||
if (difftime < -3600*2) {
|
||||
// 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 {
|
||||
} else if (difftime > 0) {
|
||||
_hasForwardInTimeFiles = true;
|
||||
}
|
||||
}
|
||||
@@ -817,7 +819,6 @@ void SyncEngine::startSync()
|
||||
|
||||
// This is used for the DiscoveryJob to be able to request the main thread/
|
||||
// to read in directory contents.
|
||||
qDebug() << Q_FUNC_INFO << _remotePath << _remoteUrl;
|
||||
_discoveryMainThread->setupHooks( discoveryJob, _remotePath);
|
||||
|
||||
// Starts the update in a seperate thread
|
||||
@@ -928,7 +929,10 @@ 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) {
|
||||
} 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.
|
||||
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;
|
||||
@@ -966,7 +970,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
|
||||
_journal->commit("post treewalk");
|
||||
|
||||
_propagator = QSharedPointer<OwncloudPropagator>(
|
||||
new OwncloudPropagator (_account, _localPath, _remoteUrl.path(), _remotePath, _journal));
|
||||
new OwncloudPropagator (_account, _localPath, _remotePath, _journal));
|
||||
connect(_propagator.data(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)),
|
||||
this, SLOT(slotItemCompleted(const SyncFileItem &, const PropagatorJob &)));
|
||||
connect(_propagator.data(), SIGNAL(progress(const SyncFileItem &,quint64)),
|
||||
@@ -1053,6 +1057,11 @@ void SyncEngine::slotFinished(bool success)
|
||||
}
|
||||
|
||||
_journal->commit("All Finished.", false);
|
||||
|
||||
// Send final progress information even if no
|
||||
// files needed propagation
|
||||
emit transmissionProgress(*_progressInfo);
|
||||
|
||||
emit treeWalkResult(_syncedItems);
|
||||
finalize(success);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class OWNCLOUDSYNC_EXPORT SyncEngine : public QObject
|
||||
Q_OBJECT
|
||||
public:
|
||||
SyncEngine(AccountPtr account, const QString &localPath,
|
||||
const QUrl &remoteURL, const QString &remotePath, SyncJournalDb *journal);
|
||||
const QString &remotePath, SyncJournalDb *journal);
|
||||
~SyncEngine();
|
||||
|
||||
static QString csyncErrorToString( CSYNC_STATUS);
|
||||
@@ -196,7 +196,6 @@ private:
|
||||
bool _needsUpdate;
|
||||
bool _syncRunning;
|
||||
QString _localPath;
|
||||
QUrl _remoteUrl;
|
||||
QString _remotePath;
|
||||
QString _remoteRootEtag;
|
||||
SyncJournalDb *_journal;
|
||||
|
||||
+38
-13
@@ -17,6 +17,7 @@
|
||||
#include "utility.h"
|
||||
|
||||
#include "version.h"
|
||||
#include "configfile.h"
|
||||
|
||||
// Note: This file must compile without QtGui
|
||||
#include <QCoreApplication>
|
||||
@@ -242,19 +243,6 @@ QString Utility::compactFormatDouble(double value, int prec, const QString& unit
|
||||
return str;
|
||||
}
|
||||
|
||||
QString Utility::toCSyncScheme(const QString &urlStr)
|
||||
{
|
||||
|
||||
QUrl url( urlStr );
|
||||
if( url.scheme() == QLatin1String("http") ) {
|
||||
url.setScheme( QLatin1String("owncloud") );
|
||||
} else {
|
||||
// connect SSL!
|
||||
url.setScheme( QLatin1String("ownclouds") );
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
QString Utility::escape(const QString &in)
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
|
||||
@@ -587,4 +575,41 @@ void Utility::sortFilenames(QStringList& fileNames)
|
||||
#endif
|
||||
}
|
||||
|
||||
QUrl Utility::concatUrlPath(const QUrl &url, const QString &concatPath,
|
||||
const QList< QPair<QString, QString> > &queryItems)
|
||||
{
|
||||
QString path = url.path();
|
||||
if (! concatPath.isEmpty()) {
|
||||
// avoid '//'
|
||||
if (path.endsWith('/') && concatPath.startsWith('/')) {
|
||||
path.chop(1);
|
||||
} // avoid missing '/'
|
||||
else if (!path.endsWith('/') && !concatPath.startsWith('/')) {
|
||||
path += QLatin1Char('/');
|
||||
}
|
||||
path += concatPath; // put the complete path together
|
||||
}
|
||||
|
||||
QUrl tmpUrl = url;
|
||||
tmpUrl.setPath(path);
|
||||
if( queryItems.size() > 0 ) {
|
||||
tmpUrl.setQueryItems(queryItems);
|
||||
}
|
||||
return tmpUrl;
|
||||
}
|
||||
|
||||
Q_GLOBAL_STATIC(QString, g_configFileName)
|
||||
|
||||
std::unique_ptr<QSettings> Utility::settingsWithGroup(const QString& group, QObject *parent)
|
||||
{
|
||||
if (g_configFileName()->isEmpty()) {
|
||||
// cache file name
|
||||
ConfigFile cfg;
|
||||
*g_configFileName() = cfg.configFile();
|
||||
}
|
||||
std::unique_ptr<QSettings> settings(new QSettings(*g_configFileName(), QSettings::IniFormat, parent));
|
||||
settings->beginGroup(group);
|
||||
return settings;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
+12
-1
@@ -22,6 +22,10 @@
|
||||
#include <QDateTime>
|
||||
#include <QElapsedTimer>
|
||||
#include <QMap>
|
||||
#include <QUrl>
|
||||
#include <memory>
|
||||
|
||||
class QSettings;
|
||||
|
||||
namespace OCC {
|
||||
|
||||
@@ -40,7 +44,6 @@ namespace Utility
|
||||
OWNCLOUDSYNC_EXPORT bool hasLaunchOnStartup(const QString &appName);
|
||||
OWNCLOUDSYNC_EXPORT void setLaunchOnStartup(const QString &appName, const QString& guiName, bool launch);
|
||||
OWNCLOUDSYNC_EXPORT qint64 freeDiskSpace(const QString &path);
|
||||
OWNCLOUDSYNC_EXPORT QString toCSyncScheme(const QString &urlStr);
|
||||
|
||||
/**
|
||||
* @brief compactFormatDouble - formats a double value human readable.
|
||||
@@ -143,6 +146,14 @@ namespace Utility
|
||||
*/
|
||||
OWNCLOUDSYNC_EXPORT void sortFilenames(QStringList& fileNames);
|
||||
|
||||
/** Appends concatPath and queryItems to the url */
|
||||
OWNCLOUDSYNC_EXPORT QUrl concatUrlPath(
|
||||
const QUrl &url, const QString &concatPath,
|
||||
const QList< QPair<QString, QString> > &queryItems = (QList<QPair<QString, QString>>()));
|
||||
|
||||
/** Returns a new settings pre-set in a specific group. The Settings will be created
|
||||
with the given parent. If no parent is specified, the caller must destroy the settings */
|
||||
OWNCLOUDSYNC_EXPORT std::unique_ptr<QSettings> settingsWithGroup(const QString& group, QObject* parent = 0);
|
||||
}
|
||||
/** @} */ // \addtogroup
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ owncloud_add_test(ExcludedFiles "")
|
||||
if(HAVE_QT5 AND NOT BUILD_WITH_QT4)
|
||||
owncloud_add_test(SyncEngine "syncenginetestutils.h")
|
||||
owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h")
|
||||
owncloud_add_test(ChunkingNg "syncenginetestutils.h")
|
||||
endif(HAVE_QT5 AND NOT BUILD_WITH_QT4)
|
||||
|
||||
SET(FolderMan_SRC ../src/gui/folderman.cpp)
|
||||
|
||||
+137
-34
@@ -18,6 +18,20 @@
|
||||
#include <QtTest>
|
||||
|
||||
static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
|
||||
static const QUrl sRootUrl2("owncloud://somehost/owncloud/remote.php/dav/files/admin/");
|
||||
static const QUrl sUploadUrl("owncloud://somehost/owncloud/remote.php/dav/uploads/admin/");
|
||||
|
||||
inline QString getFilePathFromUrl(const QUrl &url) {
|
||||
QString path = url.path();
|
||||
if (path.startsWith(sRootUrl.path()))
|
||||
return path.mid(sRootUrl.path().length());
|
||||
if (path.startsWith(sRootUrl2.path()))
|
||||
return path.mid(sRootUrl2.path().length());
|
||||
if (path.startsWith(sUploadUrl.path()))
|
||||
return path.mid(sUploadUrl.path().length());
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
inline QString generateEtag() {
|
||||
return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
|
||||
@@ -68,10 +82,15 @@ public:
|
||||
QFile file{_rootDir.filePath(relativePath)};
|
||||
QVERIFY(!file.exists());
|
||||
file.open(QFile::WriteOnly);
|
||||
file.write(QByteArray{}.fill(contentChar, size));
|
||||
QByteArray buf(1024, contentChar);
|
||||
for (int x = 0; x < size/buf.size(); ++x) {
|
||||
file.write(buf);
|
||||
}
|
||||
file.write(buf.data(), size % buf.size());
|
||||
file.close();
|
||||
// Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
|
||||
OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTime().addSecs(-30)));
|
||||
QCOMPARE(file.size(), size);
|
||||
}
|
||||
void setContents(const QString &relativePath, char contentChar) override {
|
||||
QFile file{_rootDir.filePath(relativePath)};
|
||||
@@ -283,6 +302,12 @@ public:
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isNull()); // for root, it should be empty
|
||||
const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
|
||||
Q_ASSERT(fileInfo);
|
||||
QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
|
||||
|
||||
// Don't care about the request and just return a full propfind
|
||||
const QString davUri{QStringLiteral("DAV:")};
|
||||
const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
|
||||
@@ -296,7 +321,7 @@ public:
|
||||
auto writeFileResponse = [&](const FileInfo &fileInfo) {
|
||||
xml.writeStartElement(davUri, QStringLiteral("response"));
|
||||
|
||||
xml.writeTextElement(davUri, QStringLiteral("href"), "/owncloud/remote.php/webdav/" + fileInfo.path());
|
||||
xml.writeTextElement(davUri, QStringLiteral("href"), prefix + fileInfo.path());
|
||||
xml.writeStartElement(davUri, QStringLiteral("propstat"));
|
||||
xml.writeStartElement(davUri, QStringLiteral("prop"));
|
||||
|
||||
@@ -320,11 +345,6 @@ public:
|
||||
xml.writeEndElement(); // response
|
||||
};
|
||||
|
||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
|
||||
Q_ASSERT(fileInfo);
|
||||
|
||||
writeFileResponse(*fileInfo);
|
||||
foreach(const FileInfo &childFileInfo, fileInfo->children)
|
||||
writeFileResponse(childFileInfo);
|
||||
@@ -368,8 +388,8 @@ public:
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
if ((fileInfo = remoteRootFileInfo.find(fileName))) {
|
||||
fileInfo->size = putPayload.size();
|
||||
fileInfo->contentChar = putPayload.at(0);
|
||||
@@ -386,6 +406,7 @@ public:
|
||||
}
|
||||
|
||||
Q_INVOKABLE void respond() {
|
||||
emit uploadProgress(fileInfo->size, fileInfo->size);
|
||||
setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
|
||||
setRawHeader("ETag", fileInfo->etag.toLatin1());
|
||||
setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
|
||||
@@ -410,8 +431,8 @@ public:
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
fileInfo = remoteRootFileInfo.createDir(fileName);
|
||||
|
||||
if (!fileInfo) {
|
||||
@@ -443,8 +464,8 @@ public:
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
remoteRootFileInfo.remove(fileName);
|
||||
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
|
||||
}
|
||||
@@ -470,11 +491,10 @@ public:
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
QString destPath = request.rawHeader("Destination");
|
||||
Q_ASSERT(destPath.startsWith(sRootUrl.path()));
|
||||
QString dest = destPath.mid(sRootUrl.path().length());
|
||||
QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
|
||||
Q_ASSERT(!dest.isEmpty());
|
||||
remoteRootFileInfo.rename(fileName, dest);
|
||||
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
|
||||
}
|
||||
@@ -503,8 +523,8 @@ public:
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
Q_ASSERT(request.url().path().startsWith(sRootUrl.path()));
|
||||
QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
fileInfo = remoteRootFileInfo.find(fileName);
|
||||
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
|
||||
}
|
||||
@@ -533,6 +553,79 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class FakeChunkMoveReply : public QNetworkReply
|
||||
{
|
||||
Q_OBJECT
|
||||
FileInfo *fileInfo;
|
||||
public:
|
||||
FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
|
||||
QNetworkAccessManager::Operation op, const QNetworkRequest &request,
|
||||
QObject *parent) : QNetworkReply{parent} {
|
||||
setRequest(request);
|
||||
setUrl(request.url());
|
||||
setOperation(op);
|
||||
open(QIODevice::ReadOnly);
|
||||
|
||||
QString source = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!source.isEmpty());
|
||||
Q_ASSERT(source.endsWith("/.file"));
|
||||
source = source.left(source.length() - qstrlen("/.file"));
|
||||
auto sourceFolder = uploadsFileInfo.find(source);
|
||||
Q_ASSERT(sourceFolder);
|
||||
Q_ASSERT(sourceFolder->isDir);
|
||||
int count = 0;
|
||||
int size = 0;
|
||||
char payload = '*';
|
||||
|
||||
do {
|
||||
if (!sourceFolder->children.contains(QString::number(count)))
|
||||
break;
|
||||
auto &x = sourceFolder->children[QString::number(count)];
|
||||
Q_ASSERT(!x.isDir);
|
||||
Q_ASSERT(x.size > 0); // There should not be empty chunks
|
||||
size += x.size;
|
||||
payload = x.contentChar;
|
||||
++count;
|
||||
} while(true);
|
||||
|
||||
Q_ASSERT(count > 1); // There should be at least two chunks, otherwise why would we use chunking?
|
||||
QCOMPARE(sourceFolder->children.count(), count); // There should not be holes or extra files
|
||||
|
||||
QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
|
||||
Q_ASSERT(!fileName.isEmpty());
|
||||
|
||||
if ((fileInfo = remoteRootFileInfo.find(fileName))) {
|
||||
QCOMPARE(request.rawHeader("If"), QByteArray("<" + request.rawHeader("Destination") + "> ([\"" + fileInfo->etag.toLatin1() + "\"])"));
|
||||
fileInfo->size = size;
|
||||
fileInfo->contentChar = payload;
|
||||
} else {
|
||||
Q_ASSERT(!request.hasRawHeader("If"));
|
||||
// Assume that the file is filled with the same character
|
||||
fileInfo = remoteRootFileInfo.create(fileName, size, payload);
|
||||
}
|
||||
|
||||
if (!fileInfo) {
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
Q_INVOKABLE void respond() {
|
||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
|
||||
setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
|
||||
setRawHeader("ETag", fileInfo->etag.toLatin1());
|
||||
setRawHeader("OC-FileId", fileInfo->fileId);
|
||||
emit metaDataChanged();
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void abort() override { }
|
||||
qint64 readData(char *, qint64) override { return 0; }
|
||||
};
|
||||
|
||||
|
||||
class FakeErrorReply : public QNetworkReply
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -559,33 +652,41 @@ public:
|
||||
class FakeQNAM : public QNetworkAccessManager
|
||||
{
|
||||
FileInfo _remoteRootFileInfo;
|
||||
FileInfo _uploadFileInfo;
|
||||
QStringList _errorPaths;
|
||||
public:
|
||||
FakeQNAM(FileInfo initialRoot) : _remoteRootFileInfo{std::move(initialRoot)} { }
|
||||
FileInfo ¤tRemoteState() { return _remoteRootFileInfo; }
|
||||
FileInfo &uploadState() { return _uploadFileInfo; }
|
||||
QStringList &errorPaths() { return _errorPaths; }
|
||||
|
||||
protected:
|
||||
QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
|
||||
QIODevice *outgoingData = 0) {
|
||||
const QString fileName = request.url().path().mid(sRootUrl.path().length());
|
||||
const QString fileName = getFilePathFromUrl(request.url());
|
||||
Q_ASSERT(!fileName.isNull());
|
||||
if (_errorPaths.contains(fileName))
|
||||
return new FakeErrorReply{op, request, this};
|
||||
|
||||
bool isUpload = request.url().path().startsWith(sUploadUrl.path());
|
||||
FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
|
||||
|
||||
auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
|
||||
if (verb == QLatin1String("PROPFIND"))
|
||||
// Ignore outgoingData always returning somethign good enough, works for now.
|
||||
return new FakePropfindReply{_remoteRootFileInfo, op, request, this};
|
||||
return new FakePropfindReply{info, op, request, this};
|
||||
else if (verb == QLatin1String("GET"))
|
||||
return new FakeGetReply{_remoteRootFileInfo, op, request, this};
|
||||
return new FakeGetReply{info, op, request, this};
|
||||
else if (verb == QLatin1String("PUT"))
|
||||
return new FakePutReply{_remoteRootFileInfo, op, request, outgoingData->readAll(), this};
|
||||
return new FakePutReply{info, op, request, outgoingData->readAll(), this};
|
||||
else if (verb == QLatin1String("MKCOL"))
|
||||
return new FakeMkcolReply{_remoteRootFileInfo, op, request, this};
|
||||
return new FakeMkcolReply{info, op, request, this};
|
||||
else if (verb == QLatin1String("DELETE"))
|
||||
return new FakeDeleteReply{_remoteRootFileInfo, op, request, this};
|
||||
else if (verb == QLatin1String("MOVE"))
|
||||
return new FakeMoveReply{_remoteRootFileInfo, op, request, this};
|
||||
return new FakeDeleteReply{info, op, request, this};
|
||||
else if (verb == QLatin1String("MOVE") && !isUpload)
|
||||
return new FakeMoveReply{info, op, request, this};
|
||||
else if (verb == QLatin1String("MOVE") && isUpload)
|
||||
return new FakeChunkMoveReply{info, _remoteRootFileInfo, op, request, this};
|
||||
else {
|
||||
qDebug() << verb << outgoingData;
|
||||
Q_UNREACHABLE();
|
||||
@@ -598,7 +699,6 @@ class FakeCredentials : public OCC::AbstractCredentials
|
||||
QNetworkAccessManager *_qnam;
|
||||
public:
|
||||
FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { }
|
||||
virtual bool changed(AbstractCredentials *) const { return false; }
|
||||
virtual QString authType() const { return "test"; }
|
||||
virtual QString user() const { return "admin"; }
|
||||
virtual QNetworkAccessManager* getQNAM() const { return _qnam; }
|
||||
@@ -638,7 +738,7 @@ public:
|
||||
_account->setCredentials(new FakeCredentials{_fakeQnam});
|
||||
|
||||
_journalDb.reset(new OCC::SyncJournalDb(localPath()));
|
||||
_syncEngine.reset(new OCC::SyncEngine(_account, localPath(), sRootUrl, "", _journalDb.get()));
|
||||
_syncEngine.reset(new OCC::SyncEngine(_account, localPath(), "", _journalDb.get()));
|
||||
|
||||
// A new folder will update the local file state database on first sync.
|
||||
// To have a state matching what users will encounter, we have to a sync
|
||||
@@ -658,6 +758,7 @@ public:
|
||||
}
|
||||
|
||||
FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
|
||||
FileInfo &uploadState() { return _fakeQnam->uploadState(); }
|
||||
|
||||
QStringList &serverErrorPaths() { return _fakeQnam->errorPaths(); }
|
||||
|
||||
@@ -694,14 +795,16 @@ public:
|
||||
QVERIFY(false);
|
||||
}
|
||||
|
||||
void execUntilFinished() {
|
||||
bool execUntilFinished() {
|
||||
QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
|
||||
QVERIFY(spy.wait());
|
||||
bool ok = spy.wait(60000);
|
||||
Q_ASSERT(ok && "Sync timed out");
|
||||
return spy[0][0].toBool();
|
||||
}
|
||||
|
||||
void syncOnce() {
|
||||
bool syncOnce() {
|
||||
scheduleSync();
|
||||
execUntilFinished();
|
||||
return execUntilFinished();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This software is in the public domain, furnished "as is", without technical
|
||||
* support, and with no warranty, express or implied, as to its usefulness for
|
||||
* any purpose.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <QtTest>
|
||||
#include "syncenginetestutils.h"
|
||||
#include <syncengine.h>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
class TestChunkingNG : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
|
||||
void testFileUpload() {
|
||||
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
|
||||
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
|
||||
const int size = 300 * 1000 * 1000; // 300 MB
|
||||
fakeFolder.localModifier().insert("A/a0", size);
|
||||
QVERIFY(fakeFolder.syncOnce());
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
QCOMPARE(fakeFolder.uploadState().children.count(), 1); // the transfer was done with chunking
|
||||
QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
|
||||
|
||||
// Check that another upload of the same file also work.
|
||||
fakeFolder.localModifier().appendByte("A/a0");
|
||||
QVERIFY(fakeFolder.syncOnce());
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
QCOMPARE(fakeFolder.uploadState().children.count(), 2); // the transfer was done with chunking
|
||||
}
|
||||
|
||||
|
||||
void testResume () {
|
||||
|
||||
FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
|
||||
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
|
||||
const int size = 300 * 1000 * 1000; // 300 MB
|
||||
fakeFolder.localModifier().insert("A/a0", size);
|
||||
|
||||
// Abort when the upload is at 1/3
|
||||
int sizeWhenAbort = -1;
|
||||
auto con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
|
||||
[&](const ProgressInfo &progress) {
|
||||
if (progress.completedSize() > (progress.totalSize() /3 )) {
|
||||
sizeWhenAbort = progress.completedSize();
|
||||
fakeFolder.syncEngine().abort();
|
||||
}
|
||||
});
|
||||
|
||||
QVERIFY(!fakeFolder.syncOnce()); // there should have been an error
|
||||
QObject::disconnect(con);
|
||||
QVERIFY(sizeWhenAbort > 0);
|
||||
QVERIFY(sizeWhenAbort < size);
|
||||
QCOMPARE(fakeFolder.uploadState().children.count(), 1); // the transfer was done with chunking
|
||||
auto upStateChildren = fakeFolder.uploadState().children.first().children;
|
||||
QCOMPARE(sizeWhenAbort, std::accumulate(upStateChildren.cbegin(), upStateChildren.cend(), 0,
|
||||
[](int s, const FileInfo &i) { return s + i.size; }));
|
||||
|
||||
|
||||
// Add a fake file to make sure it gets deleted
|
||||
fakeFolder.uploadState().children.first().insert("10000", size);
|
||||
QVERIFY(fakeFolder.syncOnce());
|
||||
|
||||
|
||||
|
||||
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
||||
QCOMPARE(fakeFolder.uploadState().children.count(), 1); // The same chunk id was re-used
|
||||
QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TestChunkingNG)
|
||||
#include "testchunkingng.moc"
|
||||
@@ -50,7 +50,7 @@ private slots:
|
||||
QFETCH(QueryItems, query);
|
||||
QFETCH(QString, expected);
|
||||
QUrl baseUrl("http://example.com" + base);
|
||||
QUrl resultUrl = Account::concatUrlPath(baseUrl, concat, query);
|
||||
QUrl resultUrl = Utility::concatUrlPath(baseUrl, concat, query);
|
||||
QString result = QString::fromUtf8(resultUrl.toEncoded());
|
||||
QString expectedFull = "http://example.com" + expected;
|
||||
QCOMPARE(result, expectedFull);
|
||||
|
||||
@@ -64,14 +64,6 @@ private slots:
|
||||
QVERIFY(hasLaunchOnStartup(appName) == false);
|
||||
}
|
||||
|
||||
void testToCSyncScheme()
|
||||
{
|
||||
QVERIFY(toCSyncScheme("http://example.com/owncloud/") ==
|
||||
"owncloud://example.com/owncloud/");
|
||||
QVERIFY(toCSyncScheme("https://example.com/owncloud/") ==
|
||||
"ownclouds://example.com/owncloud/");
|
||||
}
|
||||
|
||||
void testDurationToDescriptiveString()
|
||||
{
|
||||
QLocale::setDefault(QLocale("C"));
|
||||
@@ -80,7 +72,7 @@ private slots:
|
||||
quint64 sec = 1000;
|
||||
quint64 hour = 3600 * sec;
|
||||
|
||||
QDateTime current = QDateTime::currentDateTime();
|
||||
QDateTime current = QDateTime::currentDateTimeUtc();
|
||||
|
||||
QCOMPARE(durationToDescriptiveString2(0), QString("0 second(s)") );
|
||||
QCOMPARE(durationToDescriptiveString2(5), QString("0 second(s)") );
|
||||
|
||||
+588
-495
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+587
-494
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+586
-493
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+586
-493
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+582
-485
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+590
-497
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+582
-485
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+582
-485
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+583
-490
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+582
-485
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+584
-491
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+588
-497
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
+583
-490
Diferenças do arquivo suprimidas por serem muito extensas
Carregar Diff
Alguns arquivos não foram exibidos porque demasiados arquivos foram alterados neste diff Mostrar Mais
Referência em uma Nova Issue
Bloquear um usuário