mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-12-24 10:30:13 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
08cc3c8ac7
21 changed files with 293 additions and 2966 deletions
|
|
@ -764,6 +764,7 @@
|
|||
"8": "Check update failed",
|
||||
"9": "A new version is available, please browse the release announcement %s",
|
||||
"10": "Is the latest version",
|
||||
"11": "TODO",
|
||||
"12": "Query asset failed [%s]",
|
||||
"13": "Cannot create a file starting with .",
|
||||
"14": "Export failed: %s",
|
||||
|
|
@ -773,16 +774,16 @@
|
|||
"18": "Get community user account failed",
|
||||
"19": "User information has expired, please log in again",
|
||||
"20": "Cannot be converted to heading when including sub-documents",
|
||||
"21": "Backup completed",
|
||||
"22": "Backuping, please wait...",
|
||||
"23": "Backup failed: %s",
|
||||
"24": "Failed to obtain cloud sync info: %s",
|
||||
"21": "TODO",
|
||||
"22": "TODO",
|
||||
"23": "TODO",
|
||||
"24": "TODO",
|
||||
"25": "The attribute name only supports English letters and digits",
|
||||
"26": "Please initialize the data repo key first in [Settings - About - Data repo key]",
|
||||
"27": "Data integrity check failed",
|
||||
"28": "Incorrect end-to-end encryption password, unable to decrypt data",
|
||||
"28": "TODO",
|
||||
"29": "This feature requires <a target='_blank' href='https://ld246.com/subscribe/siyuan'>paid subscription</a> (If you have subscribed, please refresh or log in again in settings - account)",
|
||||
"30": "Failed to obtain cloud backup info",
|
||||
"30": "Failed to obtain cloud info",
|
||||
"31": "Account authentication failed, please login again",
|
||||
"32": "Failed to remove cloud notebook",
|
||||
"33": "Insufficient permissions to read and write files or access to the network, please check the permissions of the workspace folder and the settings of the anti-virus software/firewall. If you have run SiYuan as an administrator before, please consider switching to a new workspace directory, and do not run it as an administrator in the future (the current workspace directory may no longer be accessible by ordinary users)",
|
||||
|
|
@ -791,8 +792,8 @@
|
|||
"36": "Please check the version update in the app store",
|
||||
"37": "Do not include spaces and special symbols in the name of the cloud sync directory",
|
||||
"38": "The number of mentioned keywords [%d] is too many, currently only supports up to [512] keywords",
|
||||
"39": "E2EE password can not be blank",
|
||||
"40": "Failed to decrypt data",
|
||||
"39": "TODO",
|
||||
"40": "TODO",
|
||||
"41": "Upload completed",
|
||||
"42": "The setting is complete, the application will be closed automatically, please restart later...",
|
||||
"43": "The maximum storage capacity of cloud space [%s] has been exceeded, and data upload cannot continue",
|
||||
|
|
@ -813,14 +814,14 @@
|
|||
"58": "After the index is rebuilt, the interface will be automatically refreshed later...",
|
||||
"59": "Failed to set sync ignore list",
|
||||
"60": "Failed to get the update package: %s",
|
||||
"61": "Uploading, please wait...",
|
||||
"62": "The recovery is complete, and the index will be rebuilt...",
|
||||
"61": "TODO",
|
||||
"62": "TODO",
|
||||
"63": "Recovering, please wait...",
|
||||
"64": "There are [%d] files in total, it will take some time to index, please wait...",
|
||||
"65": "Exporting data...",
|
||||
"66": "Data file [%s] created",
|
||||
"67": "Uploaded at %s, downloaded at %s",
|
||||
"68": "Downloading, please wait...",
|
||||
"68": "TODO",
|
||||
"69": "Download completed",
|
||||
"70": "Copy notebook [%s] file [%s] failed: %s",
|
||||
"71": "Failed to insert asset file, please reopen the document",
|
||||
|
|
@ -841,11 +842,11 @@
|
|||
"86": "Please configure [Settings - About - Access authorization code]",
|
||||
"87": "Cannot move to this location",
|
||||
"88": "Finished parsing [%d] data files, remaining to be processed [%d]",
|
||||
"89": "Local data will overwrite the data in the cloud sync directory <em>%s</em>",
|
||||
"90": "Cloud synchronization directory <em>%s</em> data will overwrite local data",
|
||||
"91": "The local data and the cloud synchronization directory <em>%s</em> have the same data",
|
||||
"92": "The end-to-end encryption password is set",
|
||||
"93": "Download failed: %s",
|
||||
"89": "TODO",
|
||||
"90": "TODO",
|
||||
"91": "TODO",
|
||||
"92": "TODO",
|
||||
"93": "TODO",
|
||||
"94": "Upload failed: %s",
|
||||
"95": "Exiting...",
|
||||
"96": "Synchronization failed when exiting. Please manually perform a synchronization to ensure that the local data is consistent with the cloud data",
|
||||
|
|
@ -854,11 +855,11 @@
|
|||
"99": "Data cleaning is complete",
|
||||
"100": "Cleaning data...",
|
||||
"101": "Done setting reminder [%s]",
|
||||
"102": "Setting end-to-end encryption password...",
|
||||
"103": "[%d] data files have been downloaded, and [%d] remaining to be downloaded",
|
||||
"104": "[%d] data files have been uploaded, and [%d] remaining to be uploaded",
|
||||
"105": "Network transmission completed",
|
||||
"106": "Data download has been completed and decryption is in progress...",
|
||||
"102": "TODO",
|
||||
"103": "TODO",
|
||||
"104": "TODO",
|
||||
"105": "TODO",
|
||||
"106": "TODO",
|
||||
"107": "Moving document [%s]",
|
||||
"108": "Cleaning obsolete indexes...",
|
||||
"109": "Remove reminder completed [%s]",
|
||||
|
|
@ -881,13 +882,13 @@
|
|||
"126": "Bookmark cannot be empty",
|
||||
"127": "There are [%d] days left before the subscription expires, after which the cloud data will be completely deleted. Please visit <a target='_blank' href='https://ld246.com/subscribe/siyuan for renewal '>Here</a>, if you don't need to renew, please log out of your account to close the reminder",
|
||||
"128": "Subscription has expired, cloud data will be completely deleted after expiration. To renew, please visit <a target='_blank' href='https://ld246.com/subscribe/siyuan'>here</a> , if you don't need to renew, please log out of your account to close the reminder",
|
||||
"129": "Number of files transferred %d\nTotal bytes received %s\n",
|
||||
"130": "Number of files transferred %d\nTotal bytes sent %s\n",
|
||||
"131": "Downloaded in %.2fs",
|
||||
"132": "Uploaded in %.2fs",
|
||||
"133": "No changes to local data",
|
||||
"129": "TODO",
|
||||
"130": "TODO",
|
||||
"131": "TODO",
|
||||
"132": "TODO",
|
||||
"133": "TODO",
|
||||
"134": "In order to prevent the newly restored data from being overwritten by synchronization, the data synchronization function has been automatically suspended",
|
||||
"135": "Please make sure that all devices have been updated to the latest version, and then trigger synchronization after randomly changing a document on the main device, and finally trigger synchronization on other devices",
|
||||
"135": "TODO",
|
||||
"136": "Initializing data repository key...",
|
||||
"137": "Failed to initialize data repository key: %s",
|
||||
"138": "Data repository key is set",
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@
|
|||
"8": "Comprobar la actualización falló",
|
||||
"9": "Una nueva versión está disponible, por favor, busque el anuncio de lanzamiento %s",
|
||||
"10": "Es la última versión",
|
||||
"11": "TODO",
|
||||
"12": "Fallo en la consulta de activos [%s]",
|
||||
"13": "No se puede crear un archivo que empiece por .",
|
||||
"14": "Exportación fallida: %s",
|
||||
|
|
@ -773,16 +774,16 @@
|
|||
"18": "Falló la obtención de la cuenta de usuario de la comunidad",
|
||||
"19": "La información del usuario ha caducado, por favor, inicie sesión de nuevo",
|
||||
"20": "No se puede convertir en título al incluir subdocumentos",
|
||||
"21": "Copia de seguridad completada",
|
||||
"22": "Haciendo copia de seguridad, por favor espere...",
|
||||
"23": "Copia de seguridad fallida: %s",
|
||||
"24": "Fallo en la obtención de la información de sincronización con la nube: %s",
|
||||
"21": "TODO",
|
||||
"22": "TODO",
|
||||
"23": "TODO",
|
||||
"24": "TODO",
|
||||
"25": "El nombre del atributo sólo admite letras y dígitos en inglés",
|
||||
"26": "Por favor, inicialice primero la clave de repositorio de datos en [Configuración - Acerca de - Clave de repositorio de datos]",
|
||||
"27": "Falló la comprobación de la integridad de los datos",
|
||||
"28": "Contraseña de cifrado de extremo a extremo incorrecta, no se pueden descifrar los datos",
|
||||
"28": "TODO",
|
||||
"29": "Esta función requiere una <a target='_blank' href='https://ld246.com/subscribe/siyuan'>suscripción de pago</a> (Si se ha suscrito, actualice o vuelva a conectarse en configuración - cuenta)",
|
||||
"30": "Fallo en la obtención de la información de la copia de seguridad en la nube",
|
||||
"30": "No se pudo obtener la información de la nube",
|
||||
"31": "Falló la autentificación de la cuenta, por favor, inicie sesión de nuevo",
|
||||
"32": "Fallo en la eliminación de la libreta en la nube",
|
||||
"33": "Permisos insuficientes para leer y escribir archivos o acceso a la red, por favor comprueba los permisos de la carpeta del espacio de trabajo y la configuración del software antivirus/firewall. Si has ejecutado SiYuan como administrador antes, por favor considera cambiar a un nuevo directorio de espacio de trabajo, y no lo ejecutes como administrador en el futuro (el directorio de espacio de trabajo actual puede que ya no sea accesible por los usuarios ordinarios)",
|
||||
|
|
@ -791,8 +792,8 @@
|
|||
"36": "Por favor, comprueba la actualización de la versión en la tienda de aplicaciones",
|
||||
"37": "No incluyas espacios ni símbolos especiales en el nombre del directorio de sincronización con la nube",
|
||||
"38": "El número de palabras clave mencionadas [%d] son demasiados, actualmente solo admite hasta [512] palabras clave",
|
||||
"39": "La contraseña de E2EE no puede estar en blanco",
|
||||
"40": "Fallo en la desencriptación de datos",
|
||||
"39": "TODO",
|
||||
"40": "TODO",
|
||||
"41": "Carga completada",
|
||||
"42": "La configuración se ha completado, la aplicación se cerrará automáticamente, por favor reinicie más tarde...",
|
||||
"43": "Se ha superado la capacidad máxima de almacenamiento del espacio en la nube [%s] y la carga de datos no puede continuar",
|
||||
|
|
@ -813,14 +814,14 @@
|
|||
"58": "Después de reconstruir el índice, la interfaz se actualizará automáticamente más tarde...",
|
||||
"59": " Falló la configuración de sincronización de la lista de ignorados",
|
||||
"60": "Fallo al obtener el paquete de actualización: %s",
|
||||
"61": "Cargando, por favor espere...",
|
||||
"62": "La recuperación se ha completado y el índice se reconstruirá...",
|
||||
"61": "TODO",
|
||||
"62": "TODO",
|
||||
"63": "Recuperando, por favor espere...",
|
||||
"64": "Hay [%d] archivos en total, tardará un tiempo en indexarse, por favor espere...",
|
||||
"65": "Exportando datos...",
|
||||
"66": "Archivo de datos [%s] creado",
|
||||
"67": "Cargado en %s, descargado en %s",
|
||||
"68": "Descargando, por favor espere...",
|
||||
"68": "TODO",
|
||||
"69": "Descarga completada",
|
||||
"70": "Error en la copia del cuaderno [%s] del archivo [%s]: %s",
|
||||
"71": "Fallo en la inserción del archivo de activos, por favor reabra el documento",
|
||||
|
|
@ -841,11 +842,11 @@
|
|||
"86": "Por favor, configure [Configuración - Acerca de - Código de autorización de acceso]",
|
||||
"87": "No se puede mover a esta ubicación",
|
||||
"88": "Se ha terminado de analizar [%d] archivos de datos, quedan por procesar [%d]",
|
||||
"89": "Los datos locales sobrescribirán los datos del directorio de sincronización en la nube <em>%s</em>",
|
||||
"90": "Los datos del directorio de sincronización en la nube <em>%s</em> sobrescribirán los datos locales",
|
||||
"91": "Los datos locales y el directorio de sincronización en la nube <em>%s</em> tienen los mismos datos",
|
||||
"92": "La contraseña de encriptación de extremo a extremo está establecida",
|
||||
"93": "Descarga fallida: %s",
|
||||
"89": "TODO",
|
||||
"90": "TODO",
|
||||
"91": "TODO",
|
||||
"92": "TODO",
|
||||
"93": "TODO",
|
||||
"94": "Carga fallida: %s",
|
||||
"95": "Saliendo...",
|
||||
"96": "La sincronización falló al salir. Por favor, realice manualmente una sincronización para asegurarse de que los datos locales son coherentes con los datos de la nube",
|
||||
|
|
@ -854,11 +855,11 @@
|
|||
"99": "La limpieza de datos ha finalizado",
|
||||
"100": "Limpieza de datos...",
|
||||
"101": "El recordatorio de configuración [%s] se ha completado",
|
||||
"102": "Configurando la contraseña de cifrado de extremo a extremo...",
|
||||
"103": "[%d] archivos de datos han sido descargados, y [%d] quedan por descargar",
|
||||
"104": "[%d] archivos de datos han sido cargados, y [%d] restantes por cargar",
|
||||
"105": "Transmisión de red completada",
|
||||
"106": "La descarga de datos se ha completado y la desencriptación está en curso...",
|
||||
"102": "TODO",
|
||||
"103": "TODO",
|
||||
"104": "TODO",
|
||||
"105": "TODO",
|
||||
"106": "TODO",
|
||||
"107": "Moviendo documento [%s]",
|
||||
"108": "Limpiando índices obsoletos...",
|
||||
"109": "Eliminación de recordatorios completada [%s]",
|
||||
|
|
@ -881,13 +882,13 @@
|
|||
"126": "El marcador no puede estar vacío",
|
||||
"127": "There are [%d] days left before the subscription expires, after which the cloud data will be completely deleted. Please visit <a target='_blank' href='https://ld246.com/subscribe/siyuan'>Aquí</a> para la renovación, si no necesita renovar, salga de su cuenta para cerrar el recordatorio",
|
||||
"128": "La suscripción ha caducado, los datos de la nube se eliminarán completamente después de la expiración. Para renovar, visite <a target='_blank' href='https://ld246.com/subscribe/siyuan'>Aquí</a>, si no necesita renovar, salga de su cuenta para cerrar el recordatorio",
|
||||
"129": "Número de archivos transferidos %d\nTotal de bytes recibidos %s\n",
|
||||
"130": "Número de archivos transferidos %d\nTotal de bytes enviados %s\n",
|
||||
"131": "Descargado en %.2fs",
|
||||
"132": "Cargado en %.2fs",
|
||||
"133": "No hay cambios en los datos locales",
|
||||
"129": "TODO",
|
||||
"130": "TODO",
|
||||
"131": "TODO",
|
||||
"132": "TODO",
|
||||
"133": "TODO",
|
||||
"134": "Para evitar que los datos recién restaurados sean sobrescritos por la sincronización, se ha suspendido automáticamente la función de sincronización de datos",
|
||||
"135": "Por favor, asegúrese de que todos los dispositivos han sido actualizados a la última versión y, a continuación, active la sincronización después de cambiar aleatoriamente un documento en el dispositivo principal y, finalmente, active la sincronización en otros dispositivos",
|
||||
"135": "TODO",
|
||||
"136": "Inicializando la clave del repositorio de datos...",
|
||||
"137": "Fallo en la inicialización de la clave del repositorio de datos: %s",
|
||||
"138": "La clave del repositorio de datos está configurada",
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@
|
|||
"8": "La vérification de la mise à jour a échoué",
|
||||
"9": "Une nouvelle version est disponible, veuillez consulter l'annonce de la version %s",
|
||||
"10": "C'est la dernière version",
|
||||
"11": "TODO",
|
||||
"12": "Échec de la requête asset [%s]",
|
||||
"13": "Impossible de créer un fichier commençant par .",
|
||||
"14": "L'exportation a échoué : %s",
|
||||
|
|
@ -773,16 +774,16 @@
|
|||
"18": "Échec de la récupération du compte utilisateur communautaire",
|
||||
"19": "Les informations de l'utilisateur ont expiré, veuillez vous connecter à nouveau.",
|
||||
"20": "Ne peut pas être converti en titre lorsque des sous-documents sont inclus.",
|
||||
"21": "Sauvegarde terminée",
|
||||
"22": "En cours de sauvegarde, veuillez patienter....",
|
||||
"23": "La sauvegarde a échoué : %s",
|
||||
"24": "Impossible d'obtenir les informations de synchronisation du Cloud : %s",
|
||||
"21": "TODO",
|
||||
"22": "TODO",
|
||||
"23": "TODO",
|
||||
"24": "TODO",
|
||||
"25": "Le nom de l'attribut ne supporte que les lettres et les chiffres anglais.",
|
||||
"26": "Veuillez d'abord initialiser la clé du référentiel de données dans [Paramètres - À propos - Clé du référentiel de données]",
|
||||
"27": "La vérification de l'intégrité des données a échoué",
|
||||
"28": "Mot de passe de cryptage de bout en bout incorrect, impossible de décrypter les données",
|
||||
"28": "TODO",
|
||||
"29": "Cette fonctionnalité nécessite <a target='_blank' href='https://ld246.com/subscribe/siyuan'>un abonnement payant</a> (Si vous êtes déjà abonné, Rafraîchissez ou connectez - vous à nouveau dans Paramètres - compte)",
|
||||
"30": "Impossible d'obtenir des informations sur la sauvegarde dans le Cloud.",
|
||||
"30": "Échec de l'obtention des informations sur le cloud",
|
||||
"31": "L'authentification du compte a échoué, veuillez vous reconnecter",
|
||||
"32": "Échec de la suppression de carnet de notes du Cloud",
|
||||
"33": "Autorisations insuffisantes pour lire et écrire des fichiers ou accéder au réseau, veuillez vérifier les autorisations du dossier de l'espace de travail et les paramètres du logiciel anti-virus/pare-feu. Si vous avez déjà exécuté SiYuan en tant qu'administrateur, envisagez de passer à un nouveau répertoire d'espace de travail et ne l'exécutez plus en tant qu'administrateur à l'avenir (le répertoire d'espace de travail actuel peut ne plus être accessible aux utilisateurs ordinaires) ",
|
||||
|
|
@ -791,8 +792,8 @@
|
|||
"36": "Veuillez vérifier la mise à jour de la version dans l'App Store",
|
||||
"37": "N'incluez pas d'espaces et de symboles spéciaux dans le nom du répertoire de synchronisation cloud",
|
||||
"38": "Le nombre de mots-clés mentionnés [%d] est trop élevé, ne prend actuellement en charge que jusqu'à [512] mots-clés",
|
||||
"39": "Le mot de passe E2EE ne peut pas être vide",
|
||||
"40": "Échec du décryptage des données",
|
||||
"39": "TODO",
|
||||
"40": "TODO",
|
||||
"41": "Transfert complété",
|
||||
"42": "Le paramétrage est terminé, l'application se fermera automatiquement, merci de redémarrer plus tard...",
|
||||
"43": "La capacité de stockage maximale de l'espace cloud [%s] a été dépassée et le téléchargement des données ne peut pas continuer",
|
||||
|
|
@ -813,14 +814,14 @@
|
|||
"58": "Une fois l'index reconstruit, l'interface sera automatiquement rafraîchie ultérieurement...",
|
||||
"59": "Échec de la définition de la liste des ignores de synchronisation",
|
||||
"60": "Échec de la récupération du paquet de mise à jour : %s",
|
||||
"61": "En cours de transfert, veuillez patienter...",
|
||||
"62": "La récupération est terminée, et l'index sera reconstruit...",
|
||||
"61": "TODO",
|
||||
"62": "TODO",
|
||||
"63": "Récupération, veuillez patienter...",
|
||||
"64": "Il y a [%d] fichiers au total, l'indexation prendra un certain temps, veuillez patienter...",
|
||||
"65": "Exportation des données...",
|
||||
"66": "Fichier de données [%s] créé",
|
||||
"67": "Transféré à %s, téléchargé à %s",
|
||||
"68": "En cours de téléchargement, veuillez patienter...",
|
||||
"68": "TODO",
|
||||
"69": "Téléchargement terminé",
|
||||
"70": "La copie du carnet de notes [%s] du fichier [%s] a échoué : %s",
|
||||
"71": "L'insertion du fichier asset a échoué, veuillez rouvrir le document.",
|
||||
|
|
@ -841,11 +842,11 @@
|
|||
"86": "Veuillez configurer [Paramètres - A propos de - Code d'autorisation d'accès]",
|
||||
"87": "Impossible de se déplacer vers cet endroit",
|
||||
"88": "Fin de l'analyse des fichiers de données [%d], restant à traiter [%d]",
|
||||
"89": "Les données locales écraseront les données du répertoire de synchronisation cloud <em>%s</em>",
|
||||
"90": "Les données du répertoire de synchronisation cloud <em>%s</em> écraseront les données locales",
|
||||
"91": "Les données locales et le répertoire de synchronisation cloud <em>%s</em> ont les mêmes données",
|
||||
"92": "Le mot de passe de cryptage de bout en bout est défini",
|
||||
"93": "Le téléchargement a échoué : %s",
|
||||
"89": "TODO",
|
||||
"90": "TODO",
|
||||
"91": "TODO",
|
||||
"92": "TODO",
|
||||
"93": "TODO",
|
||||
"94": "Échec du téléchargement : %s",
|
||||
"95": "Quitter le programme...",
|
||||
"96": "La synchronisation a échoué lors de la sortie. Veuillez effectuer une synchronisation manuellement pour vous assurer que les données locales sont cohérentes avec les données du cloud",
|
||||
|
|
@ -854,11 +855,11 @@
|
|||
"99": "Le nettoyage des données est terminé",
|
||||
"100": "Nettoyage des données...",
|
||||
"101": "Rappel de réglage terminé [%s]",
|
||||
"102": "Définition du mot de passe de cryptage de bout en bout...",
|
||||
"103": "[%d] fichiers de données ont été téléchargés, il reste à télécharger [%d]",
|
||||
"104": "[%d] fichiers de données ont été téléchargés, et [%d] reste à télécharger",
|
||||
"105": "Transmission réseau terminée",
|
||||
"106": "Le téléchargement des données est terminé et le décryptage est en cours...",
|
||||
"102": "TODO",
|
||||
"103": "TODO",
|
||||
"104": "TODO",
|
||||
"105": "TODO",
|
||||
"106": "TODO",
|
||||
"107": "Déplacement du document [%s]",
|
||||
"108": "Nettoyage des index obsolètes...",
|
||||
"109": "Supprimer le rappel terminé [%s]",
|
||||
|
|
@ -881,13 +882,13 @@
|
|||
"126": "Les signets ne peuvent pas être vides",
|
||||
"127": "Il reste [%d] jours avant l'expiration de l'abonnement, après quoi les données cloud seront complètement supprimées. Veuillez visiter <a target='_blank' href='https://ld246.com/subscribe/ siyuan pour le renouvellement '>ici</a>, si vous n'avez pas besoin de renouveler, veuillez vous déconnecter de votre compte pour fermer le rappel",
|
||||
"128": "L'abonnement a expiré, les données cloud seront complètement supprimées après l'expiration. Pour renouveler, veuillez visiter <a target='_blank' href='https://ld246.com/subscribe/siyuan'>ici</a > , si vous n'avez pas besoin de renouveler, veuillez vous déconnecter de votre compte pour fermer le rappel",
|
||||
"129": "Fichier transféré %d\noctets reçus %s\n",
|
||||
"130": "Fichier transféré %d\noctets envoyés %s\n",
|
||||
"131": "Temps de téléchargement %.2fs",
|
||||
"132": "Le téléchargement a pris %.2fs",
|
||||
"133": "Aucune modification des données locales",
|
||||
"129": "TODO",
|
||||
"130": "TODO",
|
||||
"131": "TODO",
|
||||
"132": "TODO",
|
||||
"133": "TODO",
|
||||
"134": "Afin d'éviter que les données nouvellement restaurées ne soient écrasées par la synchronisation, la fonction de synchronisation des données a été automatiquement suspendue",
|
||||
"135": "Assurez-vous que tous les appareils ont été mis à jour vers la dernière version, puis déclenchez la synchronisation après avoir modifié de manière aléatoire un document sur l'appareil principal, et enfin déclenchez la synchronisation sur d'autres appareils.",
|
||||
"135": "TODO",
|
||||
"136": "Initialisation de la clé du référentiel de données...",
|
||||
"137": "Échec de l'initialisation de la clé du référentiel de données: %s",
|
||||
"138": "La clé du référentiel de données est définie",
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@
|
|||
"8": "檢查更新失敗",
|
||||
"9": "有新版本可用,請瀏覽發佈公告 %s",
|
||||
"10": "已是最新版",
|
||||
"11": "TODO",
|
||||
"12": "查詢資料檔失敗 [%s]",
|
||||
"13": "無法創建 . 開頭的文件",
|
||||
"14": "匯出失敗:%s",
|
||||
|
|
@ -773,16 +774,16 @@
|
|||
"18": "獲取社區用戶帳號失敗",
|
||||
"19": "使用者資訊已過期,請重新登入帳號",
|
||||
"20": "包含子文檔時無法轉換為標題",
|
||||
"21": "備份完畢",
|
||||
"22": "正在備份,請稍等...",
|
||||
"23": "備份失敗:%s",
|
||||
"24": "獲取雲端同步資訊失敗:%s",
|
||||
"21": "TODO",
|
||||
"22": "TODO",
|
||||
"23": "TODO",
|
||||
"24": "TODO",
|
||||
"25": "屬性名僅支援英文字母和阿拉伯數字",
|
||||
"26": "請先在 [設置 - 關於 - 數據倉庫密鑰] 中初始化數據倉庫密鑰",
|
||||
"27": "數據完整性校驗失敗",
|
||||
"28": "端到端加密密碼不正確,無法解密數據",
|
||||
"28": "TODO",
|
||||
"29": "該功能需要<a target='_blank' href='https://ld246.com/subscribe/siyuan'>付費訂閱</a>(如果你已經訂閱,請在設定-帳號中重繪或者重新登入)",
|
||||
"30": "獲取雲端備份資訊失敗",
|
||||
"30": "獲取雲端資訊失敗",
|
||||
"31": "帳號鑒權失敗,請重新登入帳號",
|
||||
"32": "刪除雲端筆記本失敗",
|
||||
"33": "讀寫檔或存取網路權限不足,請檢查工作空間資料夾權限和防毒軟體/防火牆的設置。如果你曾經使用管理員身份運行過思源,請考慮切換到新的工作空間目錄,後續請勿使用管理員身份運行(當前的工作空間目錄可能已經無法使用普通用戶存取)",
|
||||
|
|
@ -791,8 +792,8 @@
|
|||
"36": "請在應用商店中檢查版本更新",
|
||||
"37": "雲端同步目錄的名稱請勿包含空格和特殊符號",
|
||||
"38": "提及關鍵字數量 [%d] 過多,目前最多僅支援搜索 [512] 個關鍵字",
|
||||
"39": "端到端加密密碼不能為空",
|
||||
"40": "解密數據失敗",
|
||||
"39": "TODO",
|
||||
"40": "TODO",
|
||||
"41": "上傳完畢",
|
||||
"42": "設置完成,即將自動關閉應用,請稍後重新啟動...",
|
||||
"43": "已超過雲端空間最大存儲容量 [%s],無法繼續上傳數據",
|
||||
|
|
@ -813,14 +814,14 @@
|
|||
"58": "重建索引完畢,稍後將自動重新整理介面...",
|
||||
"59": "設置同步忽略列表失敗",
|
||||
"60": "獲取更新包失敗:%s",
|
||||
"61": "上傳中,請稍等...",
|
||||
"62": "恢復完畢,即將重建索引...",
|
||||
"61": "TODO",
|
||||
"62": "TODO",
|
||||
"63": "正在恢復,請稍等...",
|
||||
"64": "共有檔 [%d] 個,需要一些時間進行索引,請稍等...",
|
||||
"65": "導出數據中...",
|
||||
"66": "已創建資料檔案 [%s]",
|
||||
"67": "上傳於 %s,下載於 %s",
|
||||
"68": "下載中,請稍等...",
|
||||
"68": "TODO",
|
||||
"69": "下載完畢",
|
||||
"70": "複製筆記本 [%s] 下的檔 [%s] 失敗:%s",
|
||||
"71": "插入資料檔失敗,請重新打開文檔",
|
||||
|
|
@ -841,11 +842,11 @@
|
|||
"86": "請先配置 [設置 - 關於 - 存取授權碼]",
|
||||
"87": "無法移動到該位置",
|
||||
"88": "已完成解析 [%d] 個數據文件,剩餘待處理 [%d]",
|
||||
"89": "本地資料將覆蓋雲端同步目錄 <em>%s</em> 資料",
|
||||
"90": "雲端同步目錄 <em>%s</em> 資料將覆蓋本地資料",
|
||||
"91": "本地資料和雲端同步目錄 <em>%s</em> 資料一致",
|
||||
"92": "端到端加密密碼設置完畢",
|
||||
"93": "下載失敗:%s",
|
||||
"89": "TODO",
|
||||
"90": "TODO",
|
||||
"91": "TODO",
|
||||
"92": "TODO",
|
||||
"93": "TODO",
|
||||
"94": "上傳失敗:%s",
|
||||
"95": "正在退出...",
|
||||
"96": "退出時同步失敗,請手動執行一次同步以確保本地資料和雲端資料一致",
|
||||
|
|
@ -854,11 +855,11 @@
|
|||
"99": "清理數據完成",
|
||||
"100": "正在清理數據...",
|
||||
"101": "設置提醒完畢 [%s]",
|
||||
"102": "正在設置端到端加密密碼...",
|
||||
"103": "已下載 [%d] 個資料檔案,剩餘待下載 [%d]",
|
||||
"104": "已上傳 [%d] 個資料檔案,剩餘待上傳 [%d]",
|
||||
"105": "網絡傳輸完畢",
|
||||
"106": "資料下載已經完成,正在進行解密...",
|
||||
"102": "TODO",
|
||||
"103": "TODO",
|
||||
"104": "TODO",
|
||||
"105": "TODO",
|
||||
"106": "TODO",
|
||||
"107": "正在移動文檔 [%s]",
|
||||
"108": "正在清理已過時的索引...",
|
||||
"109": "移除提醒完畢 [%s]",
|
||||
|
|
@ -880,13 +881,13 @@
|
|||
"126": "書籤不能為空",
|
||||
"127": "訂閱距過期還剩 [%d] 天,過期後雲端數據會被徹底刪除。續訂請訪問<a target='_blank' href='https://ld246.com/subscribe/siyuan'>這裡</a>,如果不需要續訂,請登出賬號關閉該提醒",
|
||||
"128": "訂閱已經過期,過期後雲端數據會被徹底刪除。續訂請訪問<a target='_blank' href='https://ld246.com/subscribe/siyuan'>這裡</a>,如果不需要續訂,請登出賬號關閉該提醒",
|
||||
"129": "已傳輸文件 %d\n接收字節數 %s\n",
|
||||
"130": "已傳輸文件 %d\n發送字節數 %s\n",
|
||||
"131": "下載耗時 %.2fs",
|
||||
"132": "上傳耗時 %.2fs",
|
||||
"133": "本地數據暫無變更",
|
||||
"129": "TODO",
|
||||
"130": "TODO",
|
||||
"131": "TODO",
|
||||
"132": "TODO",
|
||||
"133": "TODO",
|
||||
"134": "為避免剛恢復的數據被同步覆蓋,數據同步功能已被自動暫停",
|
||||
"135": "請確保所有設備已經更新到最新版,然後在主力設備上隨意更改一個文檔後觸發同步,最後再到其他設備觸發同步",
|
||||
"135": "TODO",
|
||||
"136": "初始化數據倉庫密鑰...",
|
||||
"137": "初始化數據倉庫密鑰失敗:%s",
|
||||
"138": "數據倉庫密鑰設置完畢",
|
||||
|
|
|
|||
|
|
@ -765,6 +765,7 @@
|
|||
"8": "检查更新失败",
|
||||
"9": "有新版本可用,请浏览发布公告 %s",
|
||||
"10": "已是最新版",
|
||||
"11": "TODO",
|
||||
"12": "查询资源文件失败 [%s]",
|
||||
"13": "无法创建 . 开头的文件",
|
||||
"14": "导出失败:%s",
|
||||
|
|
@ -774,16 +775,16 @@
|
|||
"18": "获取社区用户账号失败",
|
||||
"19": "用户信息已过期,请重新登录账号",
|
||||
"20": "包含子文档时无法转换为标题",
|
||||
"21": "备份完毕",
|
||||
"22": "正在备份,请稍等...",
|
||||
"23": "备份失败:%s",
|
||||
"24": "获取云端同步信息失败:%s",
|
||||
"21": "TODO",
|
||||
"22": "TODO",
|
||||
"23": "TODO",
|
||||
"24": "TODO",
|
||||
"25": "属性名仅支持英文字母和阿拉伯数字",
|
||||
"26": "请先在 [设置 - 关于 - 数据仓库密钥] 中初始化数据仓库密钥",
|
||||
"27": "数据完整性校验失败",
|
||||
"28": "端到端加密密码不正确,无法解密数据",
|
||||
"28": "TODO",
|
||||
"29": "该功能需要<a target='_blank' href='https://ld246.com/subscribe/siyuan'>付费订阅</a>(如果你已经订阅,请在 设置 - 账号中刷新或者重新登录)",
|
||||
"30": "获取云端备份信息失败",
|
||||
"30": "获取云端信息失败",
|
||||
"31": "账号鉴权失败,请重新登录账号",
|
||||
"32": "删除云端笔记本失败",
|
||||
"33": "读写文件或访问网络权限不足,请检查工作空间文件夹权限和杀毒软件/防火墙的设置。如果你曾经使用管理员身份运行过思源,请考虑切换到新的工作空间目录,后续请勿使用管理员身份运行(当前的工作空间目录可能已经无法使用普通用户访问)",
|
||||
|
|
@ -792,8 +793,8 @@
|
|||
"36": "请在应用商店中检查版本更新",
|
||||
"37": "云端同步目录的名称请勿包含空格和特殊符号",
|
||||
"38": "提及关键字数量 [%d] 过多,目前最多仅支持搜索 [512] 个关键字",
|
||||
"39": "端到端加密密码不能为空",
|
||||
"40": "解密数据失败",
|
||||
"39": "TODO",
|
||||
"40": "TODO",
|
||||
"41": "上传完毕",
|
||||
"42": "设置完成,即将自动关闭应用,请稍后重新启动...",
|
||||
"43": "已超过云端空间最大存储容量 [%s],无法继续上传数据",
|
||||
|
|
@ -814,14 +815,14 @@
|
|||
"58": "重建索引完毕,稍后将自动刷新界面...",
|
||||
"59": "设置同步忽略列表失败",
|
||||
"60": "获取更新包失败:%s",
|
||||
"61": "上传中,请稍等...",
|
||||
"62": "恢复完毕,即将重建索引...",
|
||||
"61": "TODO",
|
||||
"62": "TODO",
|
||||
"63": "正在恢复,请稍等...",
|
||||
"64": "共有文件 [%d] 个,需要一些时间进行索引,请稍等...",
|
||||
"65": "导出数据中...",
|
||||
"66": "已创建数据文件 [%s]",
|
||||
"67": "上传于 %s,下载于 %s",
|
||||
"68": "下载中,请稍等...",
|
||||
"68": "TODO",
|
||||
"69": "下载完毕",
|
||||
"70": "复制笔记本 [%s] 下的文件 [%s] 失败:%s",
|
||||
"71": "插入资源文件失败,请重新打开文档",
|
||||
|
|
@ -842,11 +843,11 @@
|
|||
"86": "请先配置 [设置 - 关于 - 访问授权码]",
|
||||
"87": "无法移动到该位置",
|
||||
"88": "已完成解析 [%d] 个数据文件,剩余待处理 [%d]",
|
||||
"89": "本地数据将覆盖云端同步目录 <em>%s</em> 数据",
|
||||
"90": "云端同步目录 <em>%s</em> 数据将覆盖本地数据",
|
||||
"91": "本地数据和云端同步目录 <em>%s</em> 数据一致",
|
||||
"92": "端到端加密密码设置完毕",
|
||||
"93": "下载失败:%s",
|
||||
"89": "TODO",
|
||||
"90": "TODO",
|
||||
"91": "TODO",
|
||||
"92": "TODO",
|
||||
"93": "TODO",
|
||||
"94": "上传失败:%s",
|
||||
"95": "正在退出...",
|
||||
"96": "退出时同步失败,请手动执行一次同步以确保本地数据和云端数据一致",
|
||||
|
|
@ -855,11 +856,11 @@
|
|||
"99": "清理数据完成",
|
||||
"100": "正在清理数据...",
|
||||
"101": "设置提醒完毕 [%s]",
|
||||
"102": "正在设置端到端加密密码...",
|
||||
"103": "已下载 [%d] 个数据文件,剩余待下载 [%d]",
|
||||
"104": "已上传 [%d] 个数据文件,剩余待上传 [%d]",
|
||||
"105": "网络传输完毕",
|
||||
"106": "数据下载已经完成,正在进行解密...",
|
||||
"102": "TODO",
|
||||
"103": "TODO",
|
||||
"104": "TODO",
|
||||
"105": "TODO",
|
||||
"106": "TODO",
|
||||
"107": "正在移动文档 [%s]",
|
||||
"108": "正在清理已过时的索引...",
|
||||
"109": "移除提醒完毕 [%s]",
|
||||
|
|
@ -882,13 +883,13 @@
|
|||
"126": "书签不能为空",
|
||||
"127": "订阅距过期还剩 [%d] 天,过期后云端数据会被彻底删除。续订请访问<a target='_blank' href='https://ld246.com/subscribe/siyuan'>这里</a>,如果不需要续订,请登出账号关闭该提醒",
|
||||
"128": "订阅已经过期,过期后云端数据会被彻底删除。续订请访问<a target='_blank' href='https://ld246.com/subscribe/siyuan'>这里</a>,如果不需要续订,请登出账号关闭该提醒",
|
||||
"129": "已传输文件 %d\n接收字节数 %s\n",
|
||||
"130": "已传输文件 %d\n发送字节数 %s\n",
|
||||
"131": "下载耗时 %.2fs",
|
||||
"132": "上传耗时 %.2fs",
|
||||
"133": "本地数据暂无变更",
|
||||
"129": "TODO",
|
||||
"130": "TODO",
|
||||
"131": "TODO",
|
||||
"132": "TODO",
|
||||
"133": "TODO",
|
||||
"134": "为避免刚恢复的数据被同步覆盖,数据同步功能已被自动暂停",
|
||||
"135": "请确保所有设备已经更新到最新版,然后在主力设备上随意更改一个文档后触发同步,最后再到其他设备触发同步",
|
||||
"135": "TODO",
|
||||
"136": "初始化数据仓库密钥...",
|
||||
"137": "初始化数据仓库密钥失败:%s",
|
||||
"138": "数据仓库密钥设置完毕",
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/siyuan-note/siyuan/kernel/model"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
func removeCloudBackup(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
err := model.RemoveCloudBackup()
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func downloadCloudBackup(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
err := model.DownloadBackup()
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func uploadLocalBackup(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
err := model.UploadBackup()
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func recoverLocalBackup(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
err := model.RecoverLocalBackup()
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func createLocalBackup(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
err := model.CreateLocalBackup()
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalBackup(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
backup, err := model.GetLocalBackup()
|
||||
if nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
"backup": backup,
|
||||
}
|
||||
}
|
||||
|
||||
func getCloudSpace(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
sync, backup, size, assetSize, totalSize, err := model.GetCloudSpace()
|
||||
if nil != err {
|
||||
ret.Code = 1
|
||||
ret.Msg = err.Error()
|
||||
util.PushErrMsg(err.Error(), 3000)
|
||||
return
|
||||
}
|
||||
|
||||
hTrafficUploadSize := humanize.Bytes(uint64(model.Conf.User.UserTrafficUpload))
|
||||
hTrafficDownloadSize := humanize.Bytes(uint64(model.Conf.User.UserTrafficDownload))
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
"sync": sync,
|
||||
"backup": backup,
|
||||
"hAssetSize": assetSize,
|
||||
"hSize": size,
|
||||
"hTotalSize": totalSize,
|
||||
"hTrafficUploadSize": hTrafficUploadSize,
|
||||
"hTrafficDownloadSize": hTrafficDownloadSize,
|
||||
}
|
||||
}
|
||||
|
|
@ -21,11 +21,38 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/siyuan-note/siyuan/kernel/model"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
func getCloudSpace(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
sync, backup, size, assetSize, totalSize, err := model.GetCloudSpace()
|
||||
if nil != err {
|
||||
ret.Code = 1
|
||||
ret.Msg = err.Error()
|
||||
util.PushErrMsg(err.Error(), 3000)
|
||||
return
|
||||
}
|
||||
|
||||
hTrafficUploadSize := humanize.Bytes(uint64(model.Conf.User.UserTrafficUpload))
|
||||
hTrafficDownloadSize := humanize.Bytes(uint64(model.Conf.User.UserTrafficDownload))
|
||||
|
||||
ret.Data = map[string]interface{}{
|
||||
"sync": sync,
|
||||
"backup": backup,
|
||||
"hAssetSize": assetSize,
|
||||
"hSize": size,
|
||||
"hTotalSize": totalSize,
|
||||
"hTrafficUploadSize": hTrafficUploadSize,
|
||||
"hTrafficDownloadSize": hTrafficDownloadSize,
|
||||
}
|
||||
}
|
||||
|
||||
func checkoutRepo(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ func ServeAPI(ginServer *gin.Engine) {
|
|||
ginServer.Handle("POST", "/api/system/listWorkspaceDirs", model.CheckAuth, listWorkspaceDirs)
|
||||
ginServer.Handle("POST", "/api/system/setAppearanceMode", model.CheckAuth, setAppearanceMode)
|
||||
ginServer.Handle("POST", "/api/system/getSysFonts", model.CheckAuth, getSysFonts)
|
||||
ginServer.Handle("POST", "/api/system/setE2EEPasswd", model.CheckAuth, setE2EEPasswd)
|
||||
ginServer.Handle("POST", "/api/system/exit", model.CheckAuth, exit)
|
||||
ginServer.Handle("POST", "/api/system/setUILayout", model.CheckAuth, setUILayout)
|
||||
ginServer.Handle("POST", "/api/system/getConf", model.CheckAuth, getConf)
|
||||
|
|
@ -159,15 +158,7 @@ func ServeAPI(ginServer *gin.Engine) {
|
|||
|
||||
ginServer.Handle("POST", "/api/cloud/getCloudSpace", model.CheckAuth, getCloudSpace)
|
||||
|
||||
ginServer.Handle("POST", "/api/backup/getLocalBackup", model.CheckAuth, getLocalBackup)
|
||||
ginServer.Handle("POST", "/api/backup/createLocalBackup", model.CheckAuth, model.CheckReadonly, createLocalBackup)
|
||||
ginServer.Handle("POST", "/api/backup/recoverLocalBackup", model.CheckAuth, model.CheckReadonly, recoverLocalBackup)
|
||||
ginServer.Handle("POST", "/api/backup/uploadLocalBackup", model.CheckAuth, model.CheckReadonly, uploadLocalBackup)
|
||||
ginServer.Handle("POST", "/api/backup/downloadCloudBackup", model.CheckAuth, model.CheckReadonly, downloadCloudBackup)
|
||||
ginServer.Handle("POST", "/api/backup/removeCloudBackup", model.CheckAuth, model.CheckReadonly, removeCloudBackup)
|
||||
|
||||
ginServer.Handle("POST", "/api/sync/setSyncEnable", model.CheckAuth, setSyncEnable)
|
||||
ginServer.Handle("POST", "/api/sync/setSyncUseDataRepo", model.CheckAuth, setSyncUseDataRepo)
|
||||
ginServer.Handle("POST", "/api/sync/setSyncMode", model.CheckAuth, setSyncMode)
|
||||
ginServer.Handle("POST", "/api/sync/setCloudSyncDir", model.CheckAuth, setCloudSyncDir)
|
||||
ginServer.Handle("POST", "/api/sync/createCloudSyncDir", model.CheckAuth, model.CheckReadonly, createCloudSyncDir)
|
||||
|
|
@ -176,7 +167,6 @@ func ServeAPI(ginServer *gin.Engine) {
|
|||
ginServer.Handle("POST", "/api/sync/performSync", model.CheckAuth, performSync)
|
||||
ginServer.Handle("POST", "/api/sync/performBootSync", model.CheckAuth, performBootSync)
|
||||
ginServer.Handle("POST", "/api/sync/getBootSync", model.CheckAuth, getBootSync)
|
||||
ginServer.Handle("POST", "/api/sync/getSyncDirection", model.CheckAuth, getSyncDirection)
|
||||
|
||||
ginServer.Handle("POST", "/api/inbox/getShorthands", model.CheckAuth, getShorthands)
|
||||
ginServer.Handle("POST", "/api/inbox/removeShorthands", model.CheckAuth, removeShorthands)
|
||||
|
|
|
|||
|
|
@ -25,19 +25,6 @@ import (
|
|||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
func getSyncDirection(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
arg, ok := util.JsonArg(c, ret)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cloudDirName := arg["name"].(string)
|
||||
ret.Code, ret.Msg = model.GetSyncDirection(cloudDirName)
|
||||
}
|
||||
|
||||
func getBootSync(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
|
@ -121,25 +108,6 @@ func createCloudSyncDir(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func setSyncUseDataRepo(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
arg, ok := util.JsonArg(c, ret)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
enabled := arg["enabled"].(bool)
|
||||
err := model.SetSyncUseDataRepo(enabled)
|
||||
if nil != err {
|
||||
ret.Code = 1
|
||||
ret.Msg = err.Error()
|
||||
ret.Data = map[string]interface{}{"closeTimeout": 5000}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func setSyncEnable(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
|
|
|||
|
|
@ -295,77 +295,6 @@ func setNetworkProxy(c *gin.Context) {
|
|||
time.Sleep(time.Second * 3)
|
||||
}
|
||||
|
||||
func setE2EEPasswd(c *gin.Context) {
|
||||
ret := gulu.Ret.NewResult()
|
||||
defer c.JSON(http.StatusOK, ret)
|
||||
|
||||
arg, ok := util.JsonArg(c, ret)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var passwd string
|
||||
mode := int(arg["mode"].(float64))
|
||||
if 0 == mode { // 使用内建的密码生成
|
||||
passwd = model.GetBuiltInE2EEPasswd()
|
||||
} else { // 使用自定义密码
|
||||
passwd = arg["e2eePasswd"].(string)
|
||||
passwd = strings.TrimSpace(passwd)
|
||||
}
|
||||
|
||||
if "" == passwd {
|
||||
ret.Code = -1
|
||||
ret.Msg = model.Conf.Language(39)
|
||||
ret.Data = map[string]interface{}{"closeTimeout": 5000}
|
||||
return
|
||||
}
|
||||
|
||||
newPasswd := util.AESEncrypt(passwd)
|
||||
if model.Conf.E2EEPasswd == newPasswd {
|
||||
util.PushMsg(model.Conf.Language(92), 3000)
|
||||
return
|
||||
}
|
||||
|
||||
msgId := util.PushMsg(model.Conf.Language(102), 1000*7)
|
||||
if err := os.RemoveAll(model.Conf.Backup.GetSaveDir()); nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(model.Conf.Backup.GetSaveDir(), 0755); nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(model.Conf.Sync.GetSaveDir()); nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(model.Conf.Sync.GetSaveDir(), 0755); nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(filepath.Join(util.TempDir, "incremental")); nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(util.TempDir, "incremental"), 0755); nil != err {
|
||||
ret.Code = -1
|
||||
ret.Msg = err.Error()
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
model.Conf.E2EEPasswd = newPasswd
|
||||
model.Conf.E2EEPasswdMode = mode
|
||||
model.Conf.Save()
|
||||
util.PushUpdateMsg(msgId, model.Conf.Language(92), 3000)
|
||||
time.Sleep(1 * time.Second)
|
||||
model.SyncData(false, false, true)
|
||||
}
|
||||
|
||||
func addUIProcess(c *gin.Context) {
|
||||
pid := c.Query("pid")
|
||||
util.UIProcessIDs.Store(pid, true)
|
||||
|
|
|
|||
|
|
@ -23,22 +23,20 @@ import (
|
|||
)
|
||||
|
||||
type Sync struct {
|
||||
CloudName string `json:"cloudName"` // 云端同步目录名称
|
||||
Enabled bool `json:"enabled"` // 是否开启同步
|
||||
Mode int `json:"mode"` // 同步模式,0:未设置(为兼容已有配置,initConf 函数中会转换为 1),1:自动,2:手动 https://github.com/siyuan-note/siyuan/issues/5089
|
||||
Uploaded int64 `json:"uploaded"` // 最近上传时间
|
||||
Downloaded int64 `json:"downloaded"` // 最近下载时间
|
||||
Synced int64 `json:"synced"` // 最近同步时间
|
||||
Stat string `json:"stat"` // 最近同步统计信息
|
||||
UseDataRepo bool `json:"useDataRepo"` // 是否使用数据仓库同步
|
||||
CloudName string `json:"cloudName"` // 云端同步目录名称
|
||||
Enabled bool `json:"enabled"` // 是否开启同步
|
||||
Mode int `json:"mode"` // 同步模式,0:未设置(为兼容已有配置,initConf 函数中会转换为 1),1:自动,2:手动 https://github.com/siyuan-note/siyuan/issues/5089
|
||||
Uploaded int64 `json:"uploaded"` // 最近上传时间
|
||||
Downloaded int64 `json:"downloaded"` // 最近下载时间
|
||||
Synced int64 `json:"synced"` // 最近同步时间
|
||||
Stat string `json:"stat"` // 最近同步统计信息
|
||||
}
|
||||
|
||||
func NewSync() *Sync {
|
||||
return &Sync{
|
||||
CloudName: "main",
|
||||
Enabled: false,
|
||||
Mode: 1,
|
||||
UseDataRepo: true,
|
||||
CloudName: "main",
|
||||
Enabled: false,
|
||||
Mode: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,11 +38,9 @@ require (
|
|||
github.com/mssola/user_agent v0.5.3
|
||||
github.com/panjf2000/ants/v2 v2.5.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/qiniu/go-sdk/v7 v7.13.0
|
||||
github.com/radovskyb/watcher v1.0.7
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
|
||||
github.com/siyuan-note/dejavu v0.0.0-20220711060744-3fec84096399
|
||||
github.com/siyuan-note/encryption v0.0.0-20220612074546-f1dd94fe8676
|
||||
github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75
|
||||
github.com/siyuan-note/eventbus v0.0.0-20220624162334-ca7c06dc771f
|
||||
github.com/siyuan-note/filelock v0.0.0-20220704090116-54dfb035283f
|
||||
github.com/siyuan-note/httpclient v0.0.0-20220709030145-2bfb50f28e73
|
||||
|
|
@ -93,7 +91,9 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/qiniu/go-sdk/v7 v7.13.0 // indirect
|
||||
github.com/restic/chunker v0.4.0 // indirect
|
||||
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
|
|
@ -111,7 +111,7 @@ require (
|
|||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/mattn/go-sqlite3 => github.com/88250/go-sqlite3 v1.14.13-0.20220713031108-18de142395e6
|
||||
replace github.com/mattn/go-sqlite3 => github.com/88250/go-sqlite3 v1.14.13-0.20220713124603-951e48d11239
|
||||
|
||||
//replace github.com/siyuan-note/dejavu => D:\88250\dejavu
|
||||
//replace github.com/siyuan-note/httpclient => D:\88250\httpclient
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ github.com/88250/css v0.1.2 h1:+AADhEwWoGZFbUjqIsBcdnq2xfj8fDFDAGRXhBUhUY8=
|
|||
github.com/88250/css v0.1.2/go.mod h1:XfcZHQ0YuUb9VncVBurQfVyw1ZQicsB5Gc9N7BK3/ig=
|
||||
github.com/88250/flock v0.8.2 h1:LLbRJw3hoYfjD4g7DiYsYcTCCFTxm8icn/WepLlxIg0=
|
||||
github.com/88250/flock v0.8.2/go.mod h1:k+PZxETAUe4vLZx3R39ykvQCIlwHhc7AI2P2NUQV6zw=
|
||||
github.com/88250/go-sqlite3 v1.14.13-0.20220713031108-18de142395e6 h1:foyDAhSJzo12Ox/uu2LqL5pzdIO0lcME3ICBT5eiV3Y=
|
||||
github.com/88250/go-sqlite3 v1.14.13-0.20220713031108-18de142395e6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/88250/go-sqlite3 v1.14.13-0.20220713124603-951e48d11239 h1:x/Nrh630VbTneCf5AM4mA/1pB4e+lpTJXOoJeBsm+C0=
|
||||
github.com/88250/go-sqlite3 v1.14.13-0.20220713124603-951e48d11239/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/88250/gulu v1.2.0/go.mod h1:ZhEJ98UjR2y7j2toGj4/b+1rRELcZFQAPq/Yjyin2yY=
|
||||
github.com/88250/gulu v1.2.3-0.20220623112232-c502d9016360 h1:afQ0cjIA/tzwvIDFy9Jf0jFCb1FvWwKuG1QidEMMi4M=
|
||||
github.com/88250/gulu v1.2.3-0.20220623112232-c502d9016360/go.mod h1:I1qBzsksFL2ciGSuqDE7R3XW4BUMrfDgOvSXEk7FsAI=
|
||||
|
|
@ -418,8 +418,8 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJV
|
|||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/siyuan-note/dejavu v0.0.0-20220711060744-3fec84096399 h1:kg4BZwxn4A5d9YD9sx6GnyZ6o+Rn1IiuhrZ5qYrVXV0=
|
||||
github.com/siyuan-note/dejavu v0.0.0-20220711060744-3fec84096399/go.mod h1:cri+XyZAqmK5fJ98En9aOHB+YkuU8+XQcJdQ31EUhis=
|
||||
github.com/siyuan-note/encryption v0.0.0-20220612074546-f1dd94fe8676 h1:QB9TjJQFhXhZ6dAtPpY02DlzHAQm1C+WqZq6OadG8mI=
|
||||
github.com/siyuan-note/encryption v0.0.0-20220612074546-f1dd94fe8676/go.mod h1:H8fyqqAbp9XreANjeSbc72zEdFfKTXYN34tc1TjZwtw=
|
||||
github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75 h1:Bi7/7f29LW+Fm0cHc0J1NO1cZqyJwljSWVmfOqVZgaE=
|
||||
github.com/siyuan-note/encryption v0.0.0-20220713091850-5ecd92177b75/go.mod h1:H8fyqqAbp9XreANjeSbc72zEdFfKTXYN34tc1TjZwtw=
|
||||
github.com/siyuan-note/eventbus v0.0.0-20220624162334-ca7c06dc771f h1:JMobMNZ7AqaKKyEK+WeWFhix/2TDQXgPZDajU00IybU=
|
||||
github.com/siyuan-note/eventbus v0.0.0-20220624162334-ca7c06dc771f/go.mod h1:Sqo4FYX5lAXu7gWkbEdJF0e6P57tNNVV4WDKYDctokI=
|
||||
github.com/siyuan-note/filelock v0.0.0-20220704090116-54dfb035283f h1:IXZ4SWPjQLqMrBwDWcWYFE/SihUHRS9FYhk/0bnySok=
|
||||
|
|
|
|||
|
|
@ -1,601 +0,0 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/siyuan-note/encryption"
|
||||
"github.com/siyuan-note/filelock"
|
||||
"github.com/siyuan-note/siyuan/kernel/sql"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
Size int64 `json:"size"`
|
||||
HSize string `json:"hSize"`
|
||||
Updated string `json:"updated"`
|
||||
SaveDir string `json:"saveDir"` // 本地备份数据存放目录路径
|
||||
}
|
||||
|
||||
type Sync struct {
|
||||
Size int64 `json:"size"`
|
||||
HSize string `json:"hSize"`
|
||||
Updated string `json:"updated"`
|
||||
CloudName string `json:"cloudName"` // 云端同步数据存放目录名
|
||||
SaveDir string `json:"saveDir"` // 本地同步数据存放目录路径
|
||||
}
|
||||
|
||||
func RemoveCloudBackup() (err error) {
|
||||
err = removeCloudDirPath("backup")
|
||||
return
|
||||
}
|
||||
|
||||
func getCloudAvailableBackupSize() (size int64, err error) {
|
||||
sync, _, assetSize, err := getCloudSpaceOSS()
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
var syncSize int64
|
||||
if nil != sync {
|
||||
syncSize = int64(sync["size"].(float64))
|
||||
}
|
||||
size = int64(Conf.User.UserSiYuanRepoSize) - syncSize - assetSize
|
||||
return
|
||||
}
|
||||
|
||||
func GetCloudSpace() (s *Sync, b *Backup, hSize, hAssetSize, hTotalSize string, err error) {
|
||||
sync, backup, assetSize, err := getCloudSpaceOSS()
|
||||
if nil != err {
|
||||
err = errors.New(Conf.Language(30) + " " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var totalSize, syncSize, backupSize int64
|
||||
var syncUpdated, backupUpdated string
|
||||
if nil != sync {
|
||||
syncSize = int64(sync["size"].(float64))
|
||||
syncUpdated = sync["updated"].(string)
|
||||
}
|
||||
s = &Sync{
|
||||
Size: syncSize,
|
||||
HSize: humanize.Bytes(uint64(syncSize)),
|
||||
Updated: syncUpdated,
|
||||
}
|
||||
|
||||
if nil != backup {
|
||||
backupSize = int64(backup["size"].(float64))
|
||||
backupUpdated = backup["updated"].(string)
|
||||
}
|
||||
b = &Backup{
|
||||
Size: backupSize,
|
||||
HSize: humanize.Bytes(uint64(backupSize)),
|
||||
Updated: backupUpdated,
|
||||
}
|
||||
totalSize = syncSize + backupSize + assetSize
|
||||
hAssetSize = humanize.Bytes(uint64(assetSize))
|
||||
hSize = humanize.Bytes(uint64(totalSize))
|
||||
hTotalSize = byteCountSI(int64(Conf.User.UserSiYuanRepoSize))
|
||||
return
|
||||
}
|
||||
|
||||
func byteCountSI(b int64) string {
|
||||
const unit = 1000
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
func GetLocalBackup() (ret *Backup, err error) {
|
||||
backupDir := Conf.Backup.GetSaveDir()
|
||||
if err = os.MkdirAll(backupDir, 0755); nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
backup, err := os.Stat(backupDir)
|
||||
ret = &Backup{
|
||||
Updated: backup.ModTime().Format("2006-01-02 15:04:05"),
|
||||
SaveDir: Conf.Backup.GetSaveDir(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func RecoverLocalBackup() (err error) {
|
||||
if "" == Conf.E2EEPasswd {
|
||||
return errors.New(Conf.Language(11))
|
||||
}
|
||||
|
||||
writingDataLock.Lock()
|
||||
defer writingDataLock.Unlock()
|
||||
|
||||
err = filelock.ReleaseAllFileLocks()
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
sql.WaitForWritingDatabase()
|
||||
|
||||
CloseWatchAssets()
|
||||
defer WatchAssets()
|
||||
|
||||
// 使用备份恢复时自动暂停同步,避免刚刚恢复后的数据又被同步覆盖 https://github.com/siyuan-note/siyuan/issues/4773
|
||||
syncEnabled := Conf.Sync.Enabled
|
||||
Conf.Sync.Enabled = false
|
||||
Conf.Save()
|
||||
|
||||
util.PushEndlessProgress(Conf.Language(63))
|
||||
util.LogInfof("starting recovery...")
|
||||
start := time.Now()
|
||||
data := util.AESDecrypt(Conf.E2EEPasswd)
|
||||
data, _ = hex.DecodeString(string(data))
|
||||
passwd := string(data)
|
||||
decryptedDataDir, err := decryptDataDir(passwd)
|
||||
if nil != err {
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
newDataDir := filepath.Join(util.WorkspaceDir, "data.new")
|
||||
os.RemoveAll(newDataDir)
|
||||
if err = os.MkdirAll(newDataDir, 0755); nil != err {
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
|
||||
if err = stableCopy(decryptedDataDir, newDataDir); nil != err {
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
|
||||
oldDataDir := filepath.Join(util.WorkspaceDir, "data.old")
|
||||
if err = os.RemoveAll(oldDataDir); nil != err {
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
|
||||
// 备份恢复时生成历史 https://github.com/siyuan-note/siyuan/issues/4752
|
||||
if gulu.File.IsExist(util.DataDir) {
|
||||
var historyDir string
|
||||
historyDir, err = util.GetHistoryDir("backup")
|
||||
if nil != err {
|
||||
util.LogErrorf("get history dir failed: %s", err)
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
|
||||
var dirs []os.DirEntry
|
||||
dirs, err = os.ReadDir(util.DataDir)
|
||||
if nil != err {
|
||||
util.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
for _, dir := range dirs {
|
||||
from := filepath.Join(util.DataDir, dir.Name())
|
||||
to := filepath.Join(historyDir, dir.Name())
|
||||
if err = os.Rename(from, to); nil != err {
|
||||
util.LogErrorf("rename [%s] to [%s] failed: %s", from, to, err)
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gulu.File.IsExist(util.DataDir) {
|
||||
if err = os.RemoveAll(util.DataDir); nil != err {
|
||||
util.LogErrorf("remove [%s] failed: %s", util.DataDir, err)
|
||||
util.ClearPushProgress(100)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.Rename(newDataDir, util.DataDir); nil != err {
|
||||
util.ClearPushProgress(100)
|
||||
util.LogErrorf("rename data dir from [%s] to [%s] failed: %s", newDataDir, util.DataDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.Now().Sub(start).Seconds()
|
||||
size, _ := util.SizeOfDirectory(util.DataDir, false)
|
||||
sizeStr := humanize.Bytes(uint64(size))
|
||||
util.LogInfof("recovered backup [size=%s] in [%.2fs]", sizeStr, elapsed)
|
||||
|
||||
util.PushEndlessProgress(Conf.Language(62))
|
||||
time.Sleep(2 * time.Second)
|
||||
RefreshFileTree()
|
||||
if syncEnabled {
|
||||
func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
util.PushMsg(Conf.Language(134), 0)
|
||||
}()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CreateLocalBackup() (err error) {
|
||||
if "" == Conf.E2EEPasswd {
|
||||
return errors.New(Conf.Language(11))
|
||||
}
|
||||
|
||||
defer util.ClearPushProgress(100)
|
||||
util.PushEndlessProgress(Conf.Language(22))
|
||||
|
||||
writingDataLock.Lock()
|
||||
defer writingDataLock.Unlock()
|
||||
WaitForWritingFiles()
|
||||
sql.WaitForWritingDatabase()
|
||||
err = filelock.ReleaseAllFileLocks()
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
util.LogInfof("creating backup...")
|
||||
start := time.Now()
|
||||
data := util.AESDecrypt(Conf.E2EEPasswd)
|
||||
data, _ = hex.DecodeString(string(data))
|
||||
passwd := string(data)
|
||||
encryptedDataDir, err := encryptDataDir(passwd)
|
||||
if nil != err {
|
||||
util.LogErrorf("encrypt data dir failed: %s", err)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
|
||||
return
|
||||
}
|
||||
|
||||
newBackupDir := Conf.Backup.GetSaveDir() + ".new"
|
||||
os.RemoveAll(newBackupDir)
|
||||
if err = os.MkdirAll(newBackupDir, 0755); nil != err {
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
|
||||
return
|
||||
}
|
||||
|
||||
if err = stableCopy(encryptedDataDir, newBackupDir); nil != err {
|
||||
util.LogErrorf("copy encrypted data dir from [%s] to [%s] failed: %s", encryptedDataDir, newBackupDir, err)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = genCloudIndex(newBackupDir, map[string]bool{}, true)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
conf := map[string]interface{}{"updated": time.Now().UnixMilli()}
|
||||
data, err = gulu.JSON.MarshalJSON(conf)
|
||||
if nil != err {
|
||||
util.LogErrorf("marshal backup conf.json failed: %s", err)
|
||||
} else {
|
||||
confPath := filepath.Join(newBackupDir, "conf.json")
|
||||
if err = gulu.File.WriteFileSafer(confPath, data, 0644); nil != err {
|
||||
util.LogErrorf("write backup conf.json [%s] failed: %s", confPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
oldBackupDir := Conf.Backup.GetSaveDir() + ".old"
|
||||
os.RemoveAll(oldBackupDir)
|
||||
|
||||
backupDir := Conf.Backup.GetSaveDir()
|
||||
if gulu.File.IsExist(backupDir) {
|
||||
if err = os.Rename(backupDir, oldBackupDir); nil != err {
|
||||
util.LogErrorf("rename backup dir from [%s] to [%s] failed: %s", backupDir, oldBackupDir, err)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = os.Rename(newBackupDir, backupDir); nil != err {
|
||||
util.LogErrorf("rename backup dir from [%s] to [%s] failed: %s", newBackupDir, backupDir, err)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
|
||||
return
|
||||
}
|
||||
os.RemoveAll(oldBackupDir)
|
||||
elapsed := time.Now().Sub(start).Seconds()
|
||||
size, _ := util.SizeOfDirectory(backupDir, false)
|
||||
sizeStr := humanize.Bytes(uint64(size))
|
||||
util.LogInfof("created backup [size=%s] in [%.2fs]", sizeStr, elapsed)
|
||||
|
||||
util.PushEndlessProgress(Conf.Language(21))
|
||||
time.Sleep(2 * time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
func DownloadBackup() (err error) {
|
||||
// 使用路径映射文件进行解密验证 https://github.com/siyuan-note/siyuan/issues/3789
|
||||
var tmpFetchedFiles int
|
||||
var tmpTransferSize uint64
|
||||
err = ossDownload0(util.TempDir+"/backup", "backup", "/"+pathJSON, &tmpFetchedFiles, &tmpTransferSize, false)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(util.TempDir, "/backup/"+pathJSON))
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
passwdData, _ := hex.DecodeString(string(util.AESDecrypt(Conf.E2EEPasswd)))
|
||||
passwd := string(passwdData)
|
||||
data, err = encryption.AESGCMDecryptBinBytes(data, passwd)
|
||||
if nil != err {
|
||||
err = errors.New(Conf.Language(28))
|
||||
return
|
||||
}
|
||||
|
||||
localDirPath := Conf.Backup.GetSaveDir()
|
||||
util.PushEndlessProgress(Conf.Language(68))
|
||||
start := time.Now()
|
||||
fetchedFilesCount, transferSize, _, err := ossDownload(localDirPath, "backup", false)
|
||||
if nil == err {
|
||||
elapsed := time.Now().Sub(start).Seconds()
|
||||
util.LogInfof("downloaded backup [fetchedFiles=%d, transferSize=%s] in [%.2fs]", fetchedFilesCount, humanize.Bytes(transferSize), elapsed)
|
||||
util.PushEndlessProgress(Conf.Language(69))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func UploadBackup() (err error) {
|
||||
defer util.ClearPushProgress(100)
|
||||
|
||||
if err = checkUploadBackup(); nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
localDirPath := Conf.Backup.GetSaveDir()
|
||||
util.PushEndlessProgress(Conf.Language(61))
|
||||
util.LogInfof("uploading backup...")
|
||||
start := time.Now()
|
||||
wroteFiles, transferSize, err := ossUpload(true, localDirPath, "backup", "not exist", false)
|
||||
if nil == err {
|
||||
elapsed := time.Now().Sub(start).Seconds()
|
||||
util.LogInfof("uploaded backup [wroteFiles=%d, transferSize=%s] in [%.2fs]", wroteFiles, humanize.Bytes(transferSize), elapsed)
|
||||
util.PushEndlessProgress(Conf.Language(41))
|
||||
time.Sleep(2 * time.Second)
|
||||
return
|
||||
}
|
||||
err = errors.New(formatErrorMsg(err))
|
||||
return
|
||||
}
|
||||
|
||||
var pathJSON = fmt.Sprintf("%x", md5.Sum([]byte("paths.json"))) // 6952277a5a37c17aa6a7c6d86cd507b1
|
||||
|
||||
func encryptDataDir(passwd string) (encryptedDataDir string, err error) {
|
||||
encryptedDataDir = filepath.Join(util.TempDir, "incremental", "backup-encrypt")
|
||||
if err = os.RemoveAll(encryptedDataDir); nil != err {
|
||||
return
|
||||
}
|
||||
if err = os.MkdirAll(encryptedDataDir, 0755); nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
ctime := map[string]time.Time{}
|
||||
metaJSON := map[string]string{}
|
||||
filepath.Walk(util.DataDir, func(path string, info fs.FileInfo, _ error) error {
|
||||
if util.DataDir == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isCloudSkipFile(path, info) {
|
||||
if info.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
plainP := strings.TrimPrefix(path, util.DataDir+string(os.PathSeparator))
|
||||
p := plainP
|
||||
parts := strings.Split(p, string(os.PathSeparator))
|
||||
buf := bytes.Buffer{}
|
||||
for i, part := range parts {
|
||||
buf.WriteString(fmt.Sprintf("%x", sha256.Sum256([]byte(part)))[:7])
|
||||
if i < len(parts)-1 {
|
||||
buf.WriteString(string(os.PathSeparator))
|
||||
}
|
||||
}
|
||||
p = buf.String()
|
||||
metaJSON[filepath.ToSlash(p)] = filepath.ToSlash(plainP)
|
||||
p = encryptedDataDir + string(os.PathSeparator) + p
|
||||
|
||||
if info.IsDir() {
|
||||
if err = os.MkdirAll(p, 0755); nil != err {
|
||||
return io.EOF
|
||||
}
|
||||
if fi, err0 := os.Stat(path); nil == err0 {
|
||||
ctime[p] = fi.ModTime()
|
||||
}
|
||||
} else {
|
||||
if err = os.MkdirAll(filepath.Dir(p), 0755); nil != err {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
data, err0 := filelock.NoLockFileRead(path)
|
||||
if nil != err0 {
|
||||
util.LogErrorf("read file [%s] failed: %s", path, err0)
|
||||
err = err0
|
||||
return io.EOF
|
||||
}
|
||||
data, err0 = encryption.AESGCMEncryptBinBytes(data, passwd)
|
||||
if nil != err0 {
|
||||
util.LogErrorf("encrypt file [%s] failed: %s", path, err0)
|
||||
err = errors.New("encrypt file failed")
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
if err0 = gulu.File.WriteFileSafer(p, data, 0644); nil != err0 {
|
||||
util.LogErrorf("write file [%s] failed: %s", p, err0)
|
||||
err = err0
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
fi, err0 := os.Stat(path)
|
||||
if nil != err0 {
|
||||
util.LogErrorf("stat file [%s] failed: %s", path, err0)
|
||||
err = err0
|
||||
return io.EOF
|
||||
}
|
||||
ctime[p] = fi.ModTime()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
for p, t := range ctime {
|
||||
if err = os.Chtimes(p, t, t); nil != err {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否全部已经编入索引
|
||||
err = filepath.Walk(encryptedDataDir, func(path string, info fs.FileInfo, _ error) error {
|
||||
if encryptedDataDir == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
path = strings.TrimPrefix(path, encryptedDataDir+string(os.PathSeparator))
|
||||
path = filepath.ToSlash(path)
|
||||
if _, ok := metaJSON[path]; !ok {
|
||||
util.LogErrorf("not found backup path in meta [%s]", path)
|
||||
return errors.New(Conf.Language(27))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := gulu.JSON.MarshalJSON(metaJSON)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
data, err = encryption.AESGCMEncryptBinBytes(data, passwd)
|
||||
if nil != err {
|
||||
return "", errors.New("encrypt file failed")
|
||||
}
|
||||
meta := filepath.Join(encryptedDataDir, pathJSON)
|
||||
if err = gulu.File.WriteFileSafer(meta, data, 0644); nil != err {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decryptDataDir(passwd string) (decryptedDataDir string, err error) {
|
||||
decryptedDataDir = filepath.Join(util.TempDir, "incremental", "backup-decrypt")
|
||||
if err = os.RemoveAll(decryptedDataDir); nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
backupDir := Conf.Backup.GetSaveDir()
|
||||
meta := filepath.Join(util.TempDir, "backup", pathJSON)
|
||||
data, err := os.ReadFile(meta)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
data, err = encryption.AESGCMDecryptBinBytes(data, passwd)
|
||||
if nil != err {
|
||||
return "", errors.New(Conf.Language(40))
|
||||
}
|
||||
metaJSON := map[string]string{}
|
||||
if err = gulu.JSON.UnmarshalJSON(data, &metaJSON); nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
index := map[string]*CloudIndex{}
|
||||
data, err = os.ReadFile(filepath.Join(backupDir, "index.json"))
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
if err = gulu.JSON.UnmarshalJSON(data, &index); nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
err = filepath.Walk(backupDir, func(path string, info fs.FileInfo, _ error) error {
|
||||
if backupDir == path || pathJSON == info.Name() || strings.HasSuffix(info.Name(), ".json") {
|
||||
return nil
|
||||
}
|
||||
|
||||
encryptedP := strings.TrimPrefix(path, backupDir+string(os.PathSeparator))
|
||||
encryptedP = filepath.ToSlash(encryptedP)
|
||||
decryptedP := metaJSON[encryptedP]
|
||||
if "" == decryptedP {
|
||||
if gulu.File.IsDir(path) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
plainP := filepath.Join(decryptedDataDir, decryptedP)
|
||||
plainP = filepath.FromSlash(plainP)
|
||||
|
||||
if info.IsDir() {
|
||||
if err = os.MkdirAll(plainP, 0755); nil != err {
|
||||
return io.EOF
|
||||
}
|
||||
} else {
|
||||
if err = os.MkdirAll(filepath.Dir(plainP), 0755); nil != err {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
var err0 error
|
||||
data, err0 = os.ReadFile(path)
|
||||
if nil != err0 {
|
||||
util.LogErrorf("read file [%s] failed: %s", path, err0)
|
||||
err = err0
|
||||
return io.EOF
|
||||
}
|
||||
data, err0 = encryption.AESGCMDecryptBinBytes(data, passwd)
|
||||
if nil != err0 {
|
||||
util.LogErrorf("decrypt file [%s] failed: %s", path, err0)
|
||||
err = errors.New(Conf.Language(40))
|
||||
return io.EOF
|
||||
}
|
||||
if err0 = gulu.File.WriteFileSafer(plainP, data, 0644); nil != err0 {
|
||||
util.LogErrorf("write file [%s] failed: %s", plainP, err0)
|
||||
err = err0
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
var modTime int64
|
||||
idx := index["/"+encryptedP]
|
||||
if nil == idx {
|
||||
util.LogErrorf("index file [%s] not found", encryptedP)
|
||||
modTime = info.ModTime().Unix()
|
||||
} else {
|
||||
modTime = idx.Updated
|
||||
}
|
||||
if err0 = os.Chtimes(plainP, time.Unix(modTime, 0), time.Unix(modTime, 0)); nil != err0 {
|
||||
util.LogErrorf("change file [%s] time failed: %s", plainP, err0)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -349,30 +349,6 @@ func isSkipFile(filename string) bool {
|
|||
return strings.HasPrefix(filename, ".") || "node_modules" == filename || "dist" == filename || "target" == filename
|
||||
}
|
||||
|
||||
func checkUploadBackup() (err error) {
|
||||
if !IsSubscriber() {
|
||||
if "ios" == util.Container {
|
||||
return errors.New(Conf.Language(122))
|
||||
}
|
||||
return errors.New(Conf.Language(29))
|
||||
}
|
||||
|
||||
backupDir := Conf.Backup.GetSaveDir()
|
||||
backupSize, err := util.SizeOfDirectory(backupDir, false)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
cloudAvailableBackupSize, err := getCloudAvailableBackupSize()
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
if cloudAvailableBackupSize < backupSize {
|
||||
return errors.New(fmt.Sprintf(Conf.Language(43), byteCountSI(int64(Conf.User.UserSiYuanRepoSize))))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (box *Box) renameSubTrees(tree *parse.Tree) {
|
||||
subFiles := box.ListFiles(tree.Path)
|
||||
totals := len(subFiles) + 3
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ package model
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
|
@ -63,8 +61,6 @@ type AppConf struct {
|
|||
ReadOnly bool `json:"readonly"` // 是否是只读
|
||||
LocalIPs []string `json:"localIPs"` // 本地 IP 列表
|
||||
AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
|
||||
E2EEPasswd string `json:"e2eePasswd"` // 端到端加密密码,用于备份和同步
|
||||
E2EEPasswdMode int `json:"e2eePasswdMode"` // 端到端加密密码生成方式,0:自动,1:自定义
|
||||
System *conf.System `json:"system"` // 系统
|
||||
Keymap *conf.Keymap `json:"keymap"` // 快捷键
|
||||
Backup *conf.Backup `json:"backup"` // 备份配置
|
||||
|
|
@ -283,11 +279,6 @@ func InitConf() {
|
|||
Conf.AccessAuthCode = util.AccessAuthCode
|
||||
}
|
||||
|
||||
Conf.E2EEPasswdMode = 0
|
||||
if !isBuiltInE2EEPasswd() {
|
||||
Conf.E2EEPasswdMode = 1
|
||||
}
|
||||
|
||||
Conf.LocalIPs = util.GetLocalIPs()
|
||||
|
||||
Conf.Save()
|
||||
|
|
@ -555,23 +546,6 @@ func IsSubscriber() bool {
|
|||
return nil != Conf.User && (-1 == Conf.User.UserSiYuanProExpireTime || 0 < Conf.User.UserSiYuanProExpireTime) && 0 == Conf.User.UserSiYuanSubscriptionStatus
|
||||
}
|
||||
|
||||
func isBuiltInE2EEPasswd() bool {
|
||||
if nil == Conf || nil == Conf.User || "" == Conf.E2EEPasswd {
|
||||
return true
|
||||
}
|
||||
|
||||
pwd := GetBuiltInE2EEPasswd()
|
||||
return Conf.E2EEPasswd == util.AESEncrypt(pwd)
|
||||
}
|
||||
|
||||
func GetBuiltInE2EEPasswd() (ret string) {
|
||||
part1 := Conf.User.UserId[:7]
|
||||
part2 := Conf.User.UserId[7:]
|
||||
ret = part2 + part1
|
||||
ret = fmt.Sprintf("%x", sha256.Sum256([]byte(ret)))[:7]
|
||||
return
|
||||
}
|
||||
|
||||
func clearWorkspaceTemp() {
|
||||
os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
|
||||
os.RemoveAll(filepath.Join(util.TempDir, "export"))
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ func exportData(exportFolder string) (err error) {
|
|||
data := filepath.Join(util.WorkspaceDir, "data")
|
||||
if err = stableCopy(data, exportFolder); nil != err {
|
||||
util.LogErrorf("copy data dir from [%s] to [%s] failed: %s", data, baseFolderName, err)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(14), formatErrorMsg(err)))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,832 +0,0 @@
|
|||
// SiYuan - Build Your Eternal Digital Garden
|
||||
// Copyright (c) 2020-present, b3log.org
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/panjf2000/ants/v2"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
"github.com/siyuan-note/httpclient"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
)
|
||||
|
||||
func getCloudSpaceOSS() (sync, backup map[string]interface{}, assetSize int64, err error) {
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
|
||||
var resp *req.Response
|
||||
if Conf.Sync.UseDataRepo {
|
||||
resp, err = request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/dejavu/getRepoStat?uid=" + Conf.User.UserId)
|
||||
} else {
|
||||
resp, err = request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanWorkspace?uid=" + Conf.User.UserId)
|
||||
}
|
||||
if nil != err {
|
||||
util.LogErrorf("get cloud space failed: %s", err)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
util.LogErrorf("get cloud space failed: %s", result["msg"])
|
||||
err = errors.New(result["msg"].(string))
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
sync = data["sync"].(map[string]interface{})
|
||||
backup = data["backup"].(map[string]interface{})
|
||||
assetSize = int64(data["assetSize"].(float64))
|
||||
return
|
||||
}
|
||||
|
||||
func removeCloudDirPath(dirPath string) (err error) {
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"dirPath": dirPath, "token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/removeSiYuanDirPath?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("create cloud sync dir failed: %s", err)
|
||||
return ErrFailedToConnectCloudServer
|
||||
}
|
||||
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("remove cloud dir failed: %d", resp.StatusCode)
|
||||
util.LogErrorf(msg)
|
||||
err = errors.New(msg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func createCloudSyncDirOSS(name string) (err error) {
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"name": name, "token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/createSiYuanSyncDir")
|
||||
if nil != err {
|
||||
util.LogErrorf("create cloud sync dir failed: %s", err)
|
||||
return ErrFailedToConnectCloudServer
|
||||
}
|
||||
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("create cloud sync dir failed: %d", resp.StatusCode)
|
||||
util.LogErrorf(msg)
|
||||
err = errors.New(msg)
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
util.LogErrorf("create cloud sync dir failed: %s", result["msg"])
|
||||
return errors.New(result["msg"].(string))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func listCloudSyncDirOSS() (dirs []map[string]interface{}, size int64, err error) {
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetBody(map[string]interface{}{"token": Conf.User.UserToken}).
|
||||
SetResult(&result).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanSyncDirList?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("get cloud sync dirs failed: %s", err)
|
||||
return nil, 0, ErrFailedToConnectCloudServer
|
||||
}
|
||||
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("get cloud sync dirs failed: %d", resp.StatusCode)
|
||||
util.LogErrorf(msg)
|
||||
err = errors.New(msg)
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
util.LogErrorf("get cloud sync dirs failed: %s", result["msg"])
|
||||
return nil, 0, ErrFailedToConnectCloudServer
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
dataDirs := data["dirs"].([]interface{})
|
||||
for _, d := range dataDirs {
|
||||
dirs = append(dirs, d.(map[string]interface{}))
|
||||
}
|
||||
sort.Slice(dirs, func(i, j int) bool { return dirs[i]["name"].(string) < dirs[j]["name"].(string) })
|
||||
size = int64(data["size"].(float64))
|
||||
return
|
||||
}
|
||||
|
||||
func ossDownload(localDirPath, cloudDirPath string, bootOrExit bool) (fetchedFilesCount int, transferSize uint64, downloadedFiles map[string]bool, err error) {
|
||||
if !gulu.File.IsExist(localDirPath) {
|
||||
return
|
||||
}
|
||||
|
||||
cloudFileList, err := getCloudFileListOSS(cloudDirPath)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
if "backup" != cloudDirPath {
|
||||
// 将云端索引文件临时保存一下,后面下载数据时如果部分成功,需要用索引文件恢复部分成功的文件 syncDirUpsertWorkspaceData()
|
||||
|
||||
var data []byte
|
||||
data, err = gulu.JSON.MarshalJSON(cloudFileList)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
tmpSyncDir := filepath.Join(util.TempDir, "sync")
|
||||
err = os.MkdirAll(tmpSyncDir, 0755)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
tmpIndex := filepath.Join(tmpSyncDir, "index.json")
|
||||
if err = os.WriteFile(tmpIndex, data, 0644); nil != err {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
localRemoves, cloudFetches, err := localUpsertRemoveListOSS(localDirPath, cloudFileList)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
for _, localRemove := range localRemoves {
|
||||
if err = os.RemoveAll(localRemove); nil != err {
|
||||
util.LogErrorf("local remove [%s] failed: %s", localRemove, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
needPushProgress := 32 < len(cloudFetches)
|
||||
waitGroup := &sync.WaitGroup{}
|
||||
var downloadErr error
|
||||
downloadedFilesLock := sync.Mutex{}
|
||||
downloadedFiles = map[string]bool{}
|
||||
poolSize := 4
|
||||
if poolSize > len(cloudFetches)-1 /* 不计入 /.siyuan/conf.json,配置文件最后单独下载 */ {
|
||||
poolSize = len(cloudFetches)
|
||||
}
|
||||
p, _ := ants.NewPoolWithFunc(poolSize, func(arg interface{}) {
|
||||
defer waitGroup.Done()
|
||||
if nil != downloadErr {
|
||||
return // 快速失败
|
||||
}
|
||||
|
||||
fetch := arg.(string)
|
||||
err = ossDownload0(localDirPath, cloudDirPath, fetch, &fetchedFilesCount, &transferSize, bootOrExit)
|
||||
if nil != err {
|
||||
downloadErr = err // 仅记录最后一次错误
|
||||
return
|
||||
}
|
||||
downloadedFilesLock.Lock()
|
||||
downloadedFiles[fetch] = true
|
||||
downloadedFilesLock.Unlock()
|
||||
|
||||
if needPushProgress {
|
||||
msg := fmt.Sprintf(Conf.Language(103), fetchedFilesCount, len(cloudFetches)-fetchedFilesCount)
|
||||
util.PushProgress(util.PushProgressCodeProgressed, fetchedFilesCount, len(cloudFetches), msg)
|
||||
}
|
||||
if bootOrExit {
|
||||
msg := fmt.Sprintf("Downloading data from the cloud %d/%d", fetchedFilesCount, len(cloudFetches))
|
||||
util.IncBootProgress(0, msg)
|
||||
}
|
||||
})
|
||||
for _, fetch := range cloudFetches {
|
||||
if "/.siyuan/conf.json" == fetch {
|
||||
// 同步下载可能会报错,为了确保本地数据版本号不变所以不能更新配置文件,配置文件最后单独下载
|
||||
continue
|
||||
}
|
||||
if "/"+pathJSON == fetch {
|
||||
// 已经在前面验证解密的步骤中下载过了,目前位于 temp/sync/pathJSON
|
||||
continue
|
||||
}
|
||||
|
||||
waitGroup.Add(1)
|
||||
p.Invoke(fetch)
|
||||
}
|
||||
waitGroup.Wait()
|
||||
p.Release()
|
||||
if nil != downloadErr {
|
||||
err = downloadErr
|
||||
return
|
||||
}
|
||||
|
||||
if "backup" != cloudDirPath {
|
||||
err = ossDownload0(localDirPath, cloudDirPath, "/.siyuan/conf.json", &fetchedFilesCount, &transferSize, bootOrExit)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
}
|
||||
if needPushProgress {
|
||||
util.ClearPushProgress(len(cloudFetches))
|
||||
util.PushMsg(Conf.Language(106), 1000*60*10)
|
||||
}
|
||||
if bootOrExit {
|
||||
util.IncBootProgress(0, "Decrypting from sync to data...")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ossDownload0(localDirPath, cloudDirPath, fetch string, fetchedFiles *int, transferSize *uint64, bootORExit bool) (err error) {
|
||||
localFilePath := filepath.Join(localDirPath, fetch)
|
||||
remoteFileURL := path.Join(cloudDirPath, fetch)
|
||||
var result map[string]interface{}
|
||||
resp, err := httpclient.NewCloudRequest().
|
||||
SetResult(&result).
|
||||
SetBody(map[string]interface{}{"token": Conf.User.UserToken, "path": remoteFileURL}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanFile?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("download request [%s] failed: %s", remoteFileURL, err)
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New("account authentication failed, please login again")
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
util.LogErrorf("download request status code [%d]", resp.StatusCode)
|
||||
err = errors.New("download file URL failed")
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
msg := result["msg"].(string)
|
||||
util.LogErrorf("download cloud file failed: %s", msg)
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), msg))
|
||||
}
|
||||
|
||||
resultData := result["data"].(map[string]interface{})
|
||||
downloadURL := resultData["url"].(string)
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(localFilePath), 0755); nil != err {
|
||||
return
|
||||
}
|
||||
os.Remove(localFilePath)
|
||||
|
||||
if bootORExit {
|
||||
resp, err = httpclient.NewCloudFileRequest15s().Get(downloadURL)
|
||||
} else {
|
||||
resp, err = httpclient.NewCloudFileRequest2m().Get(downloadURL)
|
||||
}
|
||||
if nil != err {
|
||||
util.LogErrorf("download request [%s] failed: %s", downloadURL, err)
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
util.LogErrorf("download request [%s] status code [%d]", downloadURL, resp.StatusCode)
|
||||
err = errors.New(fmt.Sprintf("download file failed [%d]", resp.StatusCode))
|
||||
if 404 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(135))
|
||||
}
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
|
||||
data, err := resp.ToBytes()
|
||||
if nil != err {
|
||||
util.LogErrorf("download read response body data failed: %s, %s", err, string(data))
|
||||
err = errors.New("download read data failed")
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
size := int64(len(data))
|
||||
|
||||
if err = gulu.File.WriteFileSafer(localFilePath, data, 0644); nil != err {
|
||||
util.LogErrorf("write file [%s] failed: %s", localFilePath, err)
|
||||
return errors.New(fmt.Sprintf(Conf.Language(93), err))
|
||||
}
|
||||
|
||||
*fetchedFiles++
|
||||
*transferSize += uint64(size)
|
||||
return
|
||||
}
|
||||
|
||||
func ossUpload(isBackup bool, localDirPath, cloudDirPath, cloudDevice string, boot bool) (wroteFiles int, transferSize uint64, err error) {
|
||||
if !gulu.File.IsExist(localDirPath) {
|
||||
return
|
||||
}
|
||||
|
||||
localDevice := Conf.System.ID
|
||||
var localFileList, cloudFileList map[string]*CloudIndex
|
||||
if "" != localDevice && localDevice == cloudDevice && !isBackup {
|
||||
// 同一台设备连续上传,使用上一次的本地索引作为云端索引
|
||||
cloudFileList, err = getLocalFileListOSS(isBackup)
|
||||
} else {
|
||||
cloudFileList, err = getCloudFileListOSS(cloudDirPath)
|
||||
}
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
calcHash := false
|
||||
if 0 < len(cloudFileList) {
|
||||
if idx := cloudFileList["/index.json"]; nil != idx {
|
||||
calcHash = 0 == idx.Updated
|
||||
}
|
||||
}
|
||||
|
||||
excludes := getSyncExcludedList(localDirPath)
|
||||
localFileList, err = genCloudIndex(localDirPath, excludes, calcHash)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
var localUpserts, cloudRemoves []string
|
||||
localUpserts, cloudRemoves, err = cloudUpsertRemoveListOSS(localDirPath, cloudFileList, localFileList, excludes)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
err = ossRemove0(cloudDirPath, cloudRemoves)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
needPushProgress := 32 < len(localUpserts)
|
||||
waitGroup := &sync.WaitGroup{}
|
||||
var uploadErr error
|
||||
|
||||
poolSize := 4
|
||||
if poolSize > len(localUpserts) {
|
||||
poolSize = len(localUpserts)
|
||||
}
|
||||
msgId := gulu.Rand.String(7)
|
||||
p, _ := ants.NewPoolWithFunc(poolSize, func(arg interface{}) {
|
||||
defer waitGroup.Done()
|
||||
if nil != uploadErr {
|
||||
return // 快速失败
|
||||
}
|
||||
localUpsert := arg.(string)
|
||||
err = ossUpload0(localDirPath, cloudDirPath, localUpsert, &wroteFiles, &transferSize)
|
||||
if nil != err {
|
||||
uploadErr = err
|
||||
return
|
||||
}
|
||||
if needPushProgress {
|
||||
util.PushUpdateMsg(msgId, fmt.Sprintf(Conf.Language(104), wroteFiles, len(localUpserts)-wroteFiles), 1000*60)
|
||||
}
|
||||
if boot {
|
||||
msg := fmt.Sprintf("Uploading data to the cloud %d/%d", wroteFiles, len(localUpserts))
|
||||
util.IncBootProgress(0, msg)
|
||||
}
|
||||
})
|
||||
index := filepath.Join(localDirPath, "index.json")
|
||||
meta := filepath.Join(localDirPath, pathJSON)
|
||||
for _, localUpsert := range localUpserts {
|
||||
if index == localUpsert || meta == localUpsert {
|
||||
// 同步过程中断导致的一致性问题 https://github.com/siyuan-note/siyuan/issues/4912
|
||||
// index 和路径映射文件最后单独上传
|
||||
continue
|
||||
}
|
||||
|
||||
waitGroup.Add(1)
|
||||
p.Invoke(localUpsert)
|
||||
}
|
||||
waitGroup.Wait()
|
||||
p.Release()
|
||||
if nil != uploadErr {
|
||||
err = uploadErr
|
||||
return
|
||||
}
|
||||
|
||||
// 单独上传 index 和路径映射
|
||||
if uploadErr = ossUpload0(localDirPath, cloudDirPath, index, &wroteFiles, &transferSize); nil != uploadErr {
|
||||
err = uploadErr
|
||||
return
|
||||
}
|
||||
if uploadErr = ossUpload0(localDirPath, cloudDirPath, meta, &wroteFiles, &transferSize); nil != uploadErr {
|
||||
err = uploadErr
|
||||
return
|
||||
}
|
||||
|
||||
if needPushProgress {
|
||||
util.PushMsg(Conf.Language(105), 3000)
|
||||
util.PushClearMsg(msgId)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ossRemove0(cloudDirPath string, removes []string) (err error) {
|
||||
if 1 > len(removes) {
|
||||
return
|
||||
}
|
||||
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetBody(map[string]interface{}{"token": Conf.User.UserToken, "dirPath": cloudDirPath, "paths": removes}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/removeSiYuanFile?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("remove cloud file failed: %s", err)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
|
||||
if 200 != resp.StatusCode {
|
||||
msg := fmt.Sprintf("remove cloud file failed [sc=%d]", resp.StatusCode)
|
||||
util.LogErrorf(msg)
|
||||
err = errors.New(msg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ossUpload0(localDirPath, cloudDirPath, localUpsert string, wroteFiles *int, transferSize *uint64) (err error) {
|
||||
info, statErr := os.Stat(localUpsert)
|
||||
if nil != statErr {
|
||||
util.LogErrorf("stat file [%s] failed: %s", localUpsert, statErr)
|
||||
err = statErr
|
||||
return
|
||||
}
|
||||
|
||||
filename := filepath.ToSlash(strings.TrimPrefix(localUpsert, localDirPath))
|
||||
upToken, err := getOssUploadToken(filename, cloudDirPath, info.Size())
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
key := path.Join("siyuan", Conf.User.UserId, cloudDirPath, filename)
|
||||
if err = putFileToCloud(localUpsert, key, upToken); nil != err {
|
||||
util.LogErrorf("put file [%s] to cloud failed: %s", localUpsert, err)
|
||||
return errors.New(fmt.Sprintf(Conf.Language(94), err))
|
||||
}
|
||||
|
||||
//util.LogInfof("cloud wrote [%s], size [%d]", filename, info.Size())
|
||||
*wroteFiles++
|
||||
*transferSize += uint64(info.Size())
|
||||
return
|
||||
}
|
||||
|
||||
func getOssUploadToken(filename, cloudDirPath string, length int64) (ret string, err error) {
|
||||
// 因为需要指定 key,所以每次上传文件都必须在云端生成 Token,否则有安全隐患
|
||||
|
||||
var result map[string]interface{}
|
||||
req := httpclient.NewCloudRequest().
|
||||
SetResult(&result)
|
||||
req.SetBody(map[string]interface{}{
|
||||
"token": Conf.User.UserToken,
|
||||
"dirPath": cloudDirPath,
|
||||
"name": filename,
|
||||
"length": length})
|
||||
resp, err := req.Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanFileUploadToken?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("get file [%s] upload token failed: %+v", filename, err)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(94), err))
|
||||
return
|
||||
}
|
||||
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(94), Conf.Language(31)))
|
||||
return
|
||||
}
|
||||
util.LogErrorf("get file [%s] upload token failed [sc=%d]", filename, resp.StatusCode)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(94), strconv.Itoa(resp.StatusCode)))
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
msg := result["msg"].(string)
|
||||
util.LogErrorf("get file [%s] upload token failed: %s", filename, msg)
|
||||
err = errors.New(fmt.Sprintf(Conf.Language(93), msg))
|
||||
return
|
||||
}
|
||||
|
||||
resultData := result["data"].(map[string]interface{})
|
||||
ret = resultData["token"].(string)
|
||||
return
|
||||
}
|
||||
|
||||
func getCloudSyncVer(cloudDir string) (cloudSyncVer int64, err error) {
|
||||
start := time.Now()
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"syncDir": cloudDir, "token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanWorkspaceSyncVer?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("get cloud sync ver failed: %s", err)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
util.LogErrorf("get cloud sync ver failed: %d", resp.StatusCode)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
msg := result["msg"].(string)
|
||||
util.LogErrorf("get cloud sync ver failed: %s", msg)
|
||||
err = errors.New(msg)
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
cloudSyncVer = int64(data["v"].(float64))
|
||||
|
||||
if elapsed := time.Now().Sub(start).Milliseconds(); 2000 < elapsed {
|
||||
util.LogInfof("get cloud sync ver elapsed [%dms]", elapsed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getCloudSync(cloudDir string) (assetSize, backupSize int64, device string, err error) {
|
||||
start := time.Now()
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"syncDir": cloudDir, "token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanWorkspaceSync?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("get cloud sync info failed: %s", err)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
util.LogErrorf("get cloud sync info failed: %d", resp.StatusCode)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
msg := result["msg"].(string)
|
||||
util.LogErrorf("get cloud sync info failed: %s", msg)
|
||||
err = errors.New(msg)
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
assetSize = int64(data["assetSize"].(float64))
|
||||
backupSize = int64(data["backupSize"].(float64))
|
||||
if nil != data["d"] {
|
||||
device = data["d"].(string)
|
||||
}
|
||||
|
||||
if elapsed := time.Now().Sub(start).Milliseconds(); 5000 < elapsed {
|
||||
util.LogInfof("get cloud sync [%s] elapsed [%dms]", elapsed)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getLocalFileListOSS(isBackup bool) (ret map[string]*CloudIndex, err error) {
|
||||
ret = map[string]*CloudIndex{}
|
||||
dir := "sync"
|
||||
if isBackup {
|
||||
dir = "backup"
|
||||
}
|
||||
|
||||
localDirPath := filepath.Join(util.WorkspaceDir, dir)
|
||||
indexPath := filepath.Join(localDirPath, "index.json")
|
||||
if !gulu.File.IsExist(indexPath) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(indexPath)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
|
||||
err = gulu.JSON.UnmarshalJSON(data, &ret)
|
||||
return
|
||||
}
|
||||
|
||||
func getCloudFileListOSS(cloudDirPath string) (ret map[string]*CloudIndex, err error) {
|
||||
result := map[string]interface{}{}
|
||||
request := httpclient.NewCloudRequest()
|
||||
resp, err := request.
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"dirPath": cloudDirPath, "token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanFileListURL?uid=" + Conf.User.UserId)
|
||||
if nil != err {
|
||||
util.LogErrorf("get cloud file list failed: %s", err)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
util.LogErrorf("get cloud file list failed: %s", result["msg"])
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
retData := result["data"].(map[string]interface{})
|
||||
downloadURL := retData["url"].(string)
|
||||
resp, err = httpclient.NewCloudFileRequest15s().Get(downloadURL)
|
||||
if nil != err {
|
||||
util.LogErrorf("download request [%s] failed: %s", downloadURL, err)
|
||||
return
|
||||
}
|
||||
if 200 != resp.StatusCode {
|
||||
util.LogErrorf("download request [%s] status code [%d]", downloadURL, resp.StatusCode)
|
||||
err = errors.New(fmt.Sprintf("download file list failed [%d]", resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := resp.ToBytes()
|
||||
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
|
||||
util.LogErrorf("unmarshal index failed: %s", err)
|
||||
err = errors.New(fmt.Sprintf("unmarshal index failed"))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func localUpsertRemoveListOSS(localDirPath string, cloudFileList map[string]*CloudIndex) (localRemoves, cloudFetches []string, err error) {
|
||||
unchanged := map[string]bool{}
|
||||
|
||||
filepath.Walk(localDirPath, func(path string, info fs.FileInfo, err error) error {
|
||||
if localDirPath == path {
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath := filepath.ToSlash(strings.TrimPrefix(path, localDirPath))
|
||||
cloudIdx, ok := cloudFileList[relPath]
|
||||
if !ok {
|
||||
if util.CloudSingleFileMaxSizeLimit < info.Size() {
|
||||
util.LogWarnf("file [%s] larger than 100MB, ignore removing it", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
localRemoves = append(localRemoves, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
if 0 < cloudIdx.Updated {
|
||||
// 优先使用时间戳校验
|
||||
if localModTime := info.ModTime().Unix(); cloudIdx.Updated == localModTime {
|
||||
unchanged[relPath] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
localHash, hashErr := util.GetEtag(path)
|
||||
if nil != hashErr {
|
||||
err = hashErr
|
||||
return io.EOF
|
||||
}
|
||||
if cloudIdx.Hash == localHash {
|
||||
unchanged[relPath] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
for cloudPath, cloudIndex := range cloudFileList {
|
||||
if _, ok := unchanged[cloudPath]; ok {
|
||||
continue
|
||||
}
|
||||
if util.CloudSingleFileMaxSizeLimit < cloudIndex.Size {
|
||||
util.LogWarnf("cloud file [%s] larger than 100MB, ignore fetching it", cloudPath)
|
||||
continue
|
||||
}
|
||||
cloudFetches = append(cloudFetches, cloudPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cloudUpsertRemoveListOSS(localDirPath string, cloudFileList, localFileList map[string]*CloudIndex, excludes map[string]bool) (localUpserts, cloudRemoves []string, err error) {
|
||||
localUpserts, cloudRemoves = []string{}, []string{}
|
||||
|
||||
unchanged := map[string]bool{}
|
||||
for cloudFile, cloudIdx := range cloudFileList {
|
||||
localIdx := localFileList[cloudFile]
|
||||
if nil == localIdx {
|
||||
cloudRemoves = append(cloudRemoves, cloudFile)
|
||||
continue
|
||||
}
|
||||
if 0 < cloudIdx.Updated {
|
||||
// 优先使用时间戳校验
|
||||
if localIdx.Updated == cloudIdx.Updated {
|
||||
unchanged[filepath.Join(localDirPath, cloudFile)] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if localIdx.Hash == cloudIdx.Hash {
|
||||
unchanged[filepath.Join(localDirPath, cloudFile)] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
filepath.Walk(localDirPath, func(path string, info fs.FileInfo, err error) error {
|
||||
if localDirPath == path || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !unchanged[path] {
|
||||
if excludes[path] {
|
||||
return nil
|
||||
}
|
||||
if util.CloudSingleFileMaxSizeLimit < info.Size() {
|
||||
util.LogWarnf("file [%s] larger than 100MB, ignore uploading it", path)
|
||||
return nil
|
||||
}
|
||||
localUpserts = append(localUpserts, path)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func putFileToCloud(filePath, key, upToken string) (err error) {
|
||||
formUploader := storage.NewFormUploader(&storage.Config{UseHTTPS: true})
|
||||
ret := storage.PutRet{}
|
||||
err = formUploader.PutFile(context.Background(), &ret, upToken, key, filePath, nil)
|
||||
if nil != err {
|
||||
util.LogWarnf("put file [%s] to cloud failed [%s], retry it after 3s", filePath, err)
|
||||
time.Sleep(3 * time.Second)
|
||||
err = formUploader.PutFile(context.Background(), &ret, upToken, key, filePath, nil)
|
||||
if nil != err {
|
||||
return
|
||||
}
|
||||
util.LogInfof("put file [%s] to cloud retry success", filePath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -27,11 +27,13 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/88250/gulu"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/siyuan-note/dejavu"
|
||||
"github.com/siyuan-note/dejavu/entity"
|
||||
"github.com/siyuan-note/encryption"
|
||||
"github.com/siyuan-note/eventbus"
|
||||
"github.com/siyuan-note/filelock"
|
||||
"github.com/siyuan-note/httpclient"
|
||||
"github.com/siyuan-note/siyuan/kernel/cache"
|
||||
"github.com/siyuan-note/siyuan/kernel/sql"
|
||||
"github.com/siyuan-note/siyuan/kernel/util"
|
||||
|
|
@ -220,7 +222,7 @@ func DownloadCloudSnapshot(tag, id string) (err error) {
|
|||
if nil != err {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(Conf.Language(153), downloadFileCount, downloadChunkCount, byteCountSI(downloadBytes))
|
||||
msg := fmt.Sprintf(Conf.Language(153), downloadFileCount, downloadChunkCount, humanize.Bytes(uint64(downloadBytes)))
|
||||
util.PushMsg(msg, 5000)
|
||||
util.PushStatusBar(msg)
|
||||
return
|
||||
|
|
@ -251,7 +253,7 @@ func UploadCloudSnapshot(tag, id string) (err error) {
|
|||
}
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(Conf.Language(152), uploadFileCount, uploadChunkCount, byteCountSI(uploadBytes))
|
||||
msg := fmt.Sprintf(Conf.Language(152), uploadFileCount, uploadChunkCount, humanize.Bytes(uint64(uploadBytes)))
|
||||
util.PushMsg(msg, 5000)
|
||||
util.PushStatusBar(msg)
|
||||
return
|
||||
|
|
@ -457,7 +459,7 @@ func syncRepo(boot, exit, byHand bool) {
|
|||
util.LogErrorf("sync data repo failed: %s", err)
|
||||
msg := fmt.Sprintf(Conf.Language(80), formatErrorMsg(err))
|
||||
if errors.Is(err, dejavu.ErrCloudStorageSizeExceeded) {
|
||||
msg = fmt.Sprintf(Conf.Language(43), byteCountSI(int64(Conf.User.UserSiYuanRepoSize)))
|
||||
msg = fmt.Sprintf(Conf.Language(43), humanize.Bytes(uint64(Conf.User.UserSiYuanRepoSize)))
|
||||
}
|
||||
Conf.Sync.Stat = msg
|
||||
util.PushStatusBar(msg)
|
||||
|
|
@ -473,7 +475,7 @@ func syncRepo(boot, exit, byHand bool) {
|
|||
}
|
||||
util.PushStatusBar(fmt.Sprintf(Conf.Language(149), elapsed.Seconds()))
|
||||
Conf.Sync.Synced = util.CurrentTimeMillis()
|
||||
msg := fmt.Sprintf(Conf.Language(150), trafficStat.UploadFileCount, trafficStat.DownloadFileCount, trafficStat.UploadChunkCount, trafficStat.DownloadChunkCount, byteCountSI(trafficStat.UploadBytes), byteCountSI(trafficStat.DownloadBytes))
|
||||
msg := fmt.Sprintf(Conf.Language(150), trafficStat.UploadFileCount, trafficStat.DownloadFileCount, trafficStat.UploadChunkCount, trafficStat.DownloadChunkCount, humanize.Bytes(uint64(trafficStat.UploadBytes)), humanize.Bytes(uint64(trafficStat.DownloadBytes)))
|
||||
Conf.Sync.Stat = msg
|
||||
|
||||
if 1 > len(mergeResult.Upserts) && 1 > len(mergeResult.Removes) { // 没有数据变更
|
||||
|
|
@ -738,3 +740,85 @@ func buildCloudInfo() (ret *dejavu.CloudInfo, err error) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Backup struct {
|
||||
Size int64 `json:"size"`
|
||||
HSize string `json:"hSize"`
|
||||
Updated string `json:"updated"`
|
||||
SaveDir string `json:"saveDir"` // 本地备份数据存放目录路径
|
||||
}
|
||||
|
||||
type Sync struct {
|
||||
Size int64 `json:"size"`
|
||||
HSize string `json:"hSize"`
|
||||
Updated string `json:"updated"`
|
||||
CloudName string `json:"cloudName"` // 云端同步数据存放目录名
|
||||
SaveDir string `json:"saveDir"` // 本地同步数据存放目录路径
|
||||
}
|
||||
|
||||
func GetCloudSpace() (s *Sync, b *Backup, hSize, hAssetSize, hTotalSize string, err error) {
|
||||
sync, backup, assetSize, err := getCloudSpaceOSS()
|
||||
if nil != err {
|
||||
err = errors.New(Conf.Language(30) + " " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var totalSize, syncSize, backupSize int64
|
||||
var syncUpdated, backupUpdated string
|
||||
if nil != sync {
|
||||
syncSize = int64(sync["size"].(float64))
|
||||
syncUpdated = sync["updated"].(string)
|
||||
}
|
||||
s = &Sync{
|
||||
Size: syncSize,
|
||||
HSize: humanize.Bytes(uint64(syncSize)),
|
||||
Updated: syncUpdated,
|
||||
}
|
||||
|
||||
if nil != backup {
|
||||
backupSize = int64(backup["size"].(float64))
|
||||
backupUpdated = backup["updated"].(string)
|
||||
}
|
||||
b = &Backup{
|
||||
Size: backupSize,
|
||||
HSize: humanize.Bytes(uint64(backupSize)),
|
||||
Updated: backupUpdated,
|
||||
}
|
||||
totalSize = syncSize + backupSize + assetSize
|
||||
hAssetSize = humanize.Bytes(uint64(assetSize))
|
||||
hSize = humanize.Bytes(uint64(totalSize))
|
||||
hTotalSize = humanize.Bytes(uint64(Conf.User.UserSiYuanRepoSize))
|
||||
return
|
||||
}
|
||||
|
||||
func getCloudSpaceOSS() (sync, backup map[string]interface{}, assetSize int64, err error) {
|
||||
result := map[string]interface{}{}
|
||||
resp, err := httpclient.NewCloudRequest().
|
||||
SetResult(&result).
|
||||
SetBody(map[string]string{"token": Conf.User.UserToken}).
|
||||
Post(util.AliyunServer + "/apis/siyuan/dejavu/getRepoStat?uid=" + Conf.User.UserId)
|
||||
|
||||
if nil != err {
|
||||
util.LogErrorf("get cloud space failed: %s", err)
|
||||
err = ErrFailedToConnectCloudServer
|
||||
return
|
||||
}
|
||||
|
||||
if 401 == resp.StatusCode {
|
||||
err = errors.New(Conf.Language(31))
|
||||
return
|
||||
}
|
||||
|
||||
code := result["code"].(float64)
|
||||
if 0 != code {
|
||||
util.LogErrorf("get cloud space failed: %s", result["msg"])
|
||||
err = errors.New(result["msg"].(string))
|
||||
return
|
||||
}
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
sync = data["sync"].(map[string]interface{})
|
||||
backup = data["backup"].(map[string]interface{})
|
||||
assetSize = int64(data["assetSize"].(float64))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
1091
kernel/model/sync.go
1091
kernel/model/sync.go
File diff suppressed because it is too large
Load diff
|
|
@ -139,15 +139,6 @@ func SetBooted() {
|
|||
LogInfof("kernel booted")
|
||||
}
|
||||
|
||||
func GetHistoryDirNow(now, suffix string) (ret string, err error) {
|
||||
ret = filepath.Join(HistoryDir, now+"-"+suffix)
|
||||
if err = os.MkdirAll(ret, 0755); nil != err {
|
||||
LogErrorf("make history dir failed: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetHistoryDir(suffix string) (ret string, err error) {
|
||||
ret = filepath.Join(HistoryDir, time.Now().Format("2006-01-02-150405")+"-"+suffix)
|
||||
if err = os.MkdirAll(ret, 0755); nil != err {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue