1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf8 -*- 2# 3# Copyright 2020 Jose L. Domingo Lopez <github@24x7linux.com> 4# 5# This file is part of duplicity. 6# 7# Duplicity is free software; you can redistribute it and/or modify it 8# under the terms of the GNU General Public License as published by the 9# Free Software Foundation; either version 2 of the License, or (at your 10# option) any later version. 11# 12# Duplicity is distributed in the hope that it will be useful, but 13# WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15# General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with duplicity; if not, write to the Free Software Foundation, 19# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 21from __future__ import print_function 22from future import standard_library 23standard_library.install_aliases() 24 25from duplicity import util 26from duplicity.errors import BackendException 27import duplicity.backend 28 29import os 30import subprocess 31import re 32 33 34class Megav2Backend(duplicity.backend.Backend): 35 u""" Backend for MEGA.nz cloud storage, only one that works for accounts created since Nov. 2018 36 See https://github.com/megous/megatools/issues/411 for more details 37 38 This MEGA backend resorts to official tools (MEGAcmd) as available at https://mega.nz/cmd 39 MEGAcmd works through a single binary called "mega-cmd", which talks to a backend server 40 "mega-cmd-server", which keeps state (for example, persisting a session). Multiple "mega-*" 41 shell wrappers (ie. "mega-ls") exist as the user interface to "mega-cmd" and MEGA API 42 The full MEGAcmd User Guide can be found in the software's GitHub page below : 43 https://github.com/meganz/MEGAcmd/blob/master/UserGuide.md """ 44 45 def __init__(self, parsed_url): 46 duplicity.backend.Backend.__init__(self, parsed_url) 47 48 # Sanity check : ensure all the necessary "MEGAcmd" binaries exist 49 self._check_binary_exists(u'mega-login') 50 self._check_binary_exists(u'mega-logout') 51 self._check_binary_exists(u'mega-cmd') 52 self._check_binary_exists(u'mega-cmd-server') 53 self._check_binary_exists(u'mega-ls') 54 self._check_binary_exists(u'mega-mkdir') 55 self._check_binary_exists(u'mega-get') 56 self._check_binary_exists(u'mega-put') 57 self._check_binary_exists(u'mega-rm') 58 59 # "MEGAcmd" does not use a config file, however it is handy to keep one (with the old ".megarc" format) to 60 # securely store the username and password 61 self._hostname = parsed_url.hostname 62 if parsed_url.password is None: 63 self._megarc = os.getenv(u'HOME') + u'/.megav2rc' 64 try: 65 conf_file = open(self._megarc, u"r") 66 except Exception as e: 67 raise BackendException(u"No password provided in URL and MEGA configuration " 68 u"file for duplicity does not exist as '%s'" % 69 (self._megarc,)) 70 71 myvars = {} 72 for line in conf_file: 73 name, var = line.partition(u"=")[::2] 74 myvars[name.strip()] = str(var.strip()) 75 conf_file.close() 76 self._username = myvars[u"Username"] 77 self._password = myvars[u"Password"] 78 79 else: 80 self._username = parsed_url.username 81 self._password = self.get_password() 82 83 # Remote folder ("MEGAcmd" no longer shows "Root/" at the top of the hierarchy) 84 self._folder = u'/' + parsed_url.path[1:] 85 86 # Only create the remote folder if it doesn't exist yet 87 self.mega_login() 88 cmd = [u'mega-ls', self._folder] 89 try: 90 self.subprocess_popen(cmd) 91 except Exception as e: 92 self._makedir(self._folder) 93 94 def _check_binary_exists(self, cmd): 95 u'Checks that a specified command exists in the running user command path' 96 97 try: 98 # Ignore the output, as we only need the return code 99 subprocess.check_output([u'which', cmd]) 100 except Exception as e: 101 raise BackendException(u"Command '%s' not found, make sure 'MEGAcmd' tools (https://mega.nz/cmd) is " 102 u"properly installed and in the running user command path" % (cmd,)) 103 104 def _makedir(self, path): 105 u'Creates a remote directory (recursively if necessary)' 106 107 self.mega_login() 108 cmd = [u'mega-mkdir', u'-p', path] 109 try: 110 self.subprocess_popen(cmd) 111 except Exception as e: 112 error_str = str(e) 113 if u"Folder already exists" in error_str: 114 raise BackendException(u"Folder '%s' could not be created on MEGA because it already exists. " 115 u"Use another path or remove the folder in MEGA manually" % (path,)) 116 else: 117 raise BackendException(u"Folder '%s' could not be created, reason : '%s'" % (path, e)) 118 119 def _put(self, source_path, remote_filename): 120 u'''Uploads file to the specified remote folder (tries to delete it first to make 121 sure the new one can be uploaded)''' 122 123 try: 124 self.delete(remote_filename.decode()) 125 except Exception: 126 pass 127 self.upload(local_file=source_path.get_canonical().decode(), remote_file=remote_filename.decode()) 128 129 def _get(self, remote_filename, local_path): 130 u'Downloads file from the specified remote path' 131 132 self.download(remote_file=remote_filename.decode(), local_file=local_path.name.decode()) 133 134 def _list(self): 135 u'Lists files in the specified remote path' 136 137 return self.folder_contents(files_only=True) 138 139 def _delete(self, filename): 140 u'Deletes file from the specified remote path' 141 142 self.delete(remote_file=filename.decode()) 143 144 def _close(self): 145 u'Function called when backend is done being used' 146 147 cmd = [u'mega-logout'] 148 self.subprocess_popen(cmd) 149 150 def mega_login(self): 151 u'''Helper function to call from each method interacting with MEGA to make 152 sure a session already exists or one is created to start with''' 153 154 # Abort if command doesn't return in a reasonable time (somehow "mega-session" sometimes 155 # doesn't return), and create session if one doesn't exist yet 156 try: 157 subprocess.check_output(u'mega-session', timeout=30) 158 except subprocess.TimeoutExpired: 159 raise BackendException(u"Timed out while trying to determine if a MEGA session exists") 160 except Exception as e: 161 cmd = [u'mega-login', self._username, self._password] 162 try: 163 subprocess.check_output(cmd, stderr=subprocess.DEVNULL) 164 except Exception as e: 165 raise BackendException(u"Could not log in to MEGA, error : '%s'" % (e,)) 166 167 def folder_contents(self, files_only=False): 168 u'Lists contents of a remote MEGA path, optionally ignoring subdirectories' 169 170 cmd = [u'mega-ls', u'-l', self._folder] 171 172 self.mega_login() 173 files = subprocess.check_output(cmd) 174 files = files.decode().split(u'\n') 175 176 # Optionally ignore directories 177 if files_only: 178 files = [f.split()[5] for f in files if re.search(u'^-', f)] 179 180 return files 181 182 def download(self, remote_file, local_file): 183 u'Downloads a file from a remote MEGA path' 184 185 cmd = [u'mega-get', self._folder + u'/' + remote_file, local_file] 186 self.mega_login() 187 self.subprocess_popen(cmd) 188 189 def upload(self, local_file, remote_file): 190 u'Uploads a file to a remote MEGA path' 191 192 cmd = [u'mega-put', local_file, self._folder + u'/' + remote_file] 193 self.mega_login() 194 try: 195 self.subprocess_popen(cmd) 196 except Exception as e: 197 error_str = str(e) 198 if u"Reached storage quota" in error_str: 199 raise BackendException(u"MEGA account over quota, could not write file : '%s' . " 200 u"Upgrade your storage at https://mega.nz/pro or remove some data." % 201 (remote_file,)) 202 else: 203 raise BackendException(u"Failed writing file '%s' to MEGA, reason : '%s'" % (remote_file, e)) 204 205 def delete(self, remote_file): 206 u'Deletes a file from a remote MEGA path' 207 208 cmd = [u'mega-rm', u'-f', self._folder + u'/' + remote_file] 209 self.mega_login() 210 self.subprocess_popen(cmd) 211 212 213duplicity.backend.register_backend(u'megav2', Megav2Backend) 214duplicity.backend.uses_netloc.extend([u'megav2']) 215