插件位置:/www/server/panel/plugin/txcos/txcos_main.py

修改后重启宝塔服务,新代码如下:

  1. #!/usr/bin/python
  2. # coding: utf-8
  3. # -----------------------------
  4. # 宝塔Linux面板网站备份工具 - TXCOS
  5. # -----------------------------
  6. import sys, os,re
  7. if sys.version_info[0] == 2:
  8. reload(sys)
  9. sys.setdefaultencoding('utf-8')
  10. os.chdir('/www/server/panel')
  11. sys.path.insert(0,"class/")
  12. import public, db, time,re,json
  13. from qcloud_cos import CosConfig
  14. from qcloud_cos import CosS3Client
  15. # 腾讯云oss 的类
  16. class txcos_main:
  17. __oss = None
  18. __bucket_path = None
  19. __error_count = 0
  20. __secret_id = 'None'
  21. __secret_key = 'None'
  22. __region = 'None'
  23. __Bucket = 'None'
  24. __oss_path=None
  25. __error_msg = "ERROR: 无法连接腾讯云COS !"
  26. __setupPath = '/www/server/panel/plugin/txcos'
  27. __exclude = ""
  28. '''
  29. __oss : COS 客户端的对象
  30. __bucket_path : COS 的路径 例如:/ /data
  31. __error_count : 错误次数
  32. __secret_id : 腾讯云COS 的secret_id
  33. __secret_key : 腾讯云COS 的secret_key
  34. __region : 腾讯云COS 的地区
  35. __Bucket : COS 的Bucket
  36. __error_msg : 错误次数
  37. __error_msg : 错误信息
  38. '''
  39. def __init__(self):
  40. self.__conn()
  41. self.get_exclode()
  42. def set_cos(self):
  43. cfile = 'plugin/txcos/config.conf'
  44. fp = open(cfile, 'r')
  45. keys = fp.read().split('|')
  46. self.__secret_id = keys[0]
  47. self.__secret_key = keys[1]
  48. self.__region = keys[2]
  49. self.__Bucket = keys[3]
  50. self.__bucket_path = self.get_path(keys[4])
  51. try:
  52. config = CosConfig(Region=self.__region, SecretId=self.__secret_id, SecretKey=self.__secret_key, Token=None,Scheme='http')
  53. self.__oss = CosS3Client(config)
  54. except:
  55. if self.__oss == None:
  56. self.__conn()
  57. return json.dumps(self.__error_msg)
  58. def __conn(self):
  59. cfile = 'plugin/txcos/config.conf'
  60. if not os.path.exists(cfile): cfile = 'data/txcos.conf';
  61. if not os.path.exists(cfile): public.writeFile(cfile, '');
  62. fp = open(cfile, 'r')
  63. if not fp:
  64. print('ERROR: 请检查atxcos.conf文件中是否有腾讯云secret_id相关信息!')
  65. keys = fp.read().split('|')
  66. if len(keys) < 4:
  67. keys = ['', '', '', '', '/']
  68. if len(keys) < 5: keys.append('/');
  69. self.__secret_id = keys[0]
  70. self.__secret_key = keys[1]
  71. self.__region = keys[2]
  72. self.__Bucket = keys[3]
  73. self.__bucket_path = self.get_path(keys[4])
  74. try:
  75. config = CosConfig(Region=self.__region, SecretId=self.__secret_id, SecretKey=self.__secret_key, Token=None,Scheme='http')
  76. self.__oss = CosS3Client(config)
  77. except:
  78. if self.__oss==None:return json.dumps(self.__error_msg)
  79. def GetConfig(self, get):
  80. path = self.__setupPath + '/config.conf'
  81. if not os.path.exists(path):
  82. if os.path.exists('conf/atxcos.conf'): public.writeFile(path, public.readFile('conf/atxcos.conf'));
  83. if not os.path.exists(path): return ['', '', '', '', '/'];
  84. conf = public.readFile(path)
  85. if not conf: return ['', '', '', '', '/']
  86. result = conf.split('|')
  87. if len(result) < 5: result.append('/');
  88. return result
  89. def SetConfig(self, get):
  90. path = self.__setupPath + '/config.conf'
  91. conf = get.secret_id.strip() + '|' + get.secret_key.strip() + '|' + get.region.strip() + '|' + get.Bucket.strip() + '|' + get.bucket_path.strip()
  92. public.writeFile(path, conf)
  93. return public.returnMsg(True, '设置成功!')
  94. # 上传文件
  95. def upload_file(self, filename):
  96. if not self.__oss:
  97. self.set_cos()
  98. if not self.__oss:
  99. return False
  100. try:
  101. # 断点续传
  102. filepath,key= os.path.split(filename)
  103. print(key)
  104. key = self.__bucket_path + key
  105. print(key)
  106. # 短点续传
  107. response = self.__oss.upload_file(
  108. Bucket=self.__Bucket,
  109. Key=key,
  110. MAXThread=10,
  111. PartSize=5,
  112. LocalFilePath=filename)
  113. except:
  114. time.sleep(1)
  115. self.__error_count += 1
  116. if self.__error_count < 2: # 重试2次
  117. self.sync_date()
  118. self.upload_file(filename)
  119. print(self.__error_msg)
  120. return None
  121. def create_dir(self, get):
  122. if not self.__oss:
  123. self.set_cos()
  124. if not self.__oss:
  125. return False
  126. path = self.get_path(get.path + get.dirname)
  127. filename = '/tmp/dirname.pl'
  128. public.writeFile(filename, '')
  129. response = self.__oss.put_object(
  130. Bucket=self.__Bucket,
  131. Body=b'',
  132. Key=path,
  133. )
  134. os.remove(filename)
  135. return public.returnMsg(True, '创建成功!')
  136. def get_list(self, get):
  137. try:
  138. data = []
  139. dir_list = []
  140. #path = self.get_path(get.path) ant修改
  141. path = self.__bucket_path+self.get_path(get.path)
  142. if 'Contents' in self.__oss.list_objects(Bucket=self.__Bucket, MaxKeys=100, Delimiter='/', Prefix=path):
  143. for b in self.__oss.list_objects(Bucket=self.__Bucket, MaxKeys=100, Delimiter='/', Prefix=path)['Contents']:
  144. tmp = {}
  145. b['Key'] = b['Key'].replace(path, '')
  146. if not b['Key']: continue
  147. tmp['name'] = b['Key']
  148. tmp['size'] = b['Size']
  149. tmp['type'] = b['StorageClass']
  150. tmp['download'] = self.download_file(path + b['Key'])
  151. tmp['time'] = b['LastModified']
  152. data.append(tmp)
  153. else:
  154. pass
  155. if 'CommonPrefixes' in self.__oss.list_objects(Bucket=self.__Bucket, MaxKeys=100, Delimiter='/', Prefix=path):
  156. for i in self.__oss.list_objects(Bucket=self.__Bucket, MaxKeys=100, Delimiter='/', Prefix=path)['CommonPrefixes']:
  157. if not i['Prefix']: continue
  158. dir_dir=i['Prefix'].split('/')[-2]+'/'
  159. dir_list.append(dir_dir)
  160. else:
  161. pass
  162. mlist = {}
  163. mlist['path'] = get.path
  164. mlist['list'] = data
  165. mlist['dir'] = dir_list
  166. return mlist
  167. except:
  168. mlist = {}
  169. if self.__oss:
  170. mlist['status']=True
  171. else:
  172. mlist['status']=False
  173. mlist['path'] = get.path
  174. mlist['list'] = data
  175. mlist['dir'] = dir_list
  176. return mlist
  177. def sync_date(self):
  178. public.ExecShell("ntpdate 0.asia.pool.ntp.org")
  179. def download_file(self, filename, Expired=300):
  180. if not self.__oss:
  181. self.set_cos()
  182. if self.__oss:
  183. return False
  184. try:
  185. response = self.__oss.get_presigned_download_url(
  186. Bucket=self.__Bucket,
  187. Key=filename
  188. )
  189. response = re.findall('([^?]*)?.*', response)[0]
  190. return response
  191. except:
  192. print(self.__error_msg)
  193. return None
  194. def get_path(self, path):
  195. if path == '/': path = '';
  196. if path[:1] == '/':
  197. path = path[1:]
  198. if path[-1:] != '/': path += '/';
  199. return path
  200. def delete_file(self, filename):
  201. if not self.__oss:
  202. self.set_cos()
  203. if not self.__oss:
  204. return False
  205. try:
  206. response = self.__oss.delete_object(
  207. Bucket=self.__Bucket,
  208. Key=filename
  209. )
  210. return response
  211. except Exception as ex:
  212. self.__error_count += 1
  213. if self.__error_count < 2:
  214. self.sync_date()
  215. self.delete_file(filename)
  216. print(self.__error_msg)
  217. return None
  218. # 删除文件
  219. def remove_file(self, get):
  220. if not self.__oss:
  221. self.set_cos()
  222. if not self.__oss:
  223. return False
  224. path = self.get_path(get.path)
  225. filename = path + get.filename
  226. self.delete_file(filename)
  227. return public.returnMsg(True, '删除文件成功!')
  228. # 备份网站
  229. def backupSite(self, name, count):
  230. # self.set_cos()
  231. # self.__conn();
  232. if not self.__oss:
  233. self.set_cos()
  234. if not self.__oss:
  235. return False
  236. sql = db.Sql();
  237. path = sql.table('sites').where('name=?', (name,)).getField('path');
  238. startTime = time.time();
  239. if not path:
  240. endDate = time.strftime('%Y/%m/%d %X', time.localtime())
  241. log = "网站[" + name + "]不存在!"
  242. print("★[" + endDate + "] " + log)
  243. print("----------------------------------------------------------------------------")
  244. return;
  245. backup_path = sql.table('config').where("id=?", (1,)).getField('backup_path') + '/site';
  246. if not os.path.exists(backup_path): public.ExecShell("mkdir -p " + backup_path);
  247. filename = backup_path + "/Web_" + name + "_" + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.tar.gz'
  248. public.ExecShell("cd " + os.path.dirname(path) + " && tar zcvf '" + filename + "' '" + os.path.basename(path) + "'" + self.__exclude + " > /dev/null")
  249. endDate = time.strftime('%Y/%m/%d %X', time.localtime())
  250. time.sleep(1)
  251. if not os.path.exists(filename):
  252. log = "网站[" + name + "]备份失败!"
  253. print("★[" + endDate + "] " + log)
  254. print("----------------------------------------------------------------------------")
  255. return;
  256. #ant修改
  257. # if self.__bucket_path != '': self.__bucket_path = 'backup/site' + '/';
  258. self.__bucket_path = self.__bucket_path +'backup/site/';
  259. # 上传文件
  260. self.upload_file(filename);
  261. outTime = time.time() - startTime
  262. pid = sql.table('sites').where('name=?', (name,)).getField('id');
  263. sql.table('backup').add('type,name,pid,filename,addtime,size',
  264. ('0', os.path.basename(filename), pid, 'txcos', endDate, os.path.getsize(filename)))
  265. log = "网站[" + name + "]已成功备份到腾讯云COS,用时[" + str(round(outTime, 2)) + "]秒";
  266. public.WriteLog('计划任务', log)
  267. print("★[" + endDate + "] " + log)
  268. print("|---保留最新的[" + count + "]份备份")
  269. print("|---文件名:" + os.path.basename(filename))
  270. if self.__exclude: print(u"|---排除规则: " + self.__exclude)
  271. # 清理本地文件
  272. public.ExecShell("rm -f " + filename)
  273. # 清理多余备份
  274. backups = sql.table('backup').where('type=? and pid=? and filename=?', ('0', pid,'txcos')).field('id,name,filename').select();
  275. num = len(backups) - int(count)
  276. if num > 0:
  277. for backup in backups:
  278. if os.path.exists(backup['filename']):
  279. public.ExecShell("rm -f " + backup['filename']);
  280. self.delete_file(self.__bucket_path + backup['name']);
  281. sql.table('backup').where('id=?', (backup['id'],)).delete();
  282. num -= 1;
  283. print("|---已清理过期备份文件:" + backup['name'])
  284. if num < 1: break;
  285. return None
  286. # 备份数据库
  287. def backupDatabase(self, name, count):
  288. if not self.__oss:
  289. self.set_cos()
  290. if not self.__oss:
  291. return False
  292. sql = db.Sql();
  293. path = sql.table('databases').where('name=?', (name,)).getField('id');
  294. startTime = time.time();
  295. if not path:
  296. endDate = time.strftime('%Y/%m/%d %X', time.localtime())
  297. log = "数据库[" + name + "]不存在!"
  298. print("★[" + endDate + "] " + log)
  299. print("----------------------------------------------------------------------------")
  300. return;
  301. backup_path = sql.table('config').where("id=?", (1,)).getField('backup_path') + '/database';
  302. if not os.path.exists(backup_path): public.ExecShell("mkdir -p " + backup_path);
  303. filename = backup_path + "/Db_" + name + "_" + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + ".sql.gz"
  304. import re
  305. mysql_root = sql.table('config').where("id=?", (1,)).getField('mysql_root')
  306. mycnf = public.readFile('/etc/my.cnf');
  307. rep = "\[mysqldump\]\nuser=root"
  308. sea = "[mysqldump]\n"
  309. subStr = sea + "user=root\npassword=" + mysql_root + "\n";
  310. mycnf = mycnf.replace(sea, subStr)
  311. if len(mycnf) > 100:
  312. public.writeFile('/etc/my.cnf', mycnf);
  313. public.ExecShell("/www/server/mysql/bin/mysqldump --default-character-set="+ self.get_database_character(name) +" --force --opt " + name + " | gzip > " + filename)
  314. if not os.path.exists(filename):
  315. endDate = time.strftime('%Y/%m/%d %X', time.localtime())
  316. log = "数据库[" + name + "]备份失败!"
  317. print("★[" + endDate + "] " + log)
  318. print("----------------------------------------------------------------------------")
  319. return;
  320. mycnf = public.readFile('/etc/my.cnf');
  321. mycnf = mycnf.replace(subStr, sea)
  322. if len(mycnf) > 100:
  323. public.writeFile('/etc/my.cnf', mycnf);
  324. # 上传 ant修改
  325. #if self.__bucket_path != '': self.__bucket_path = '' + 'backup/databases' + '/';
  326. self.__bucket_path = self.__bucket_path + 'backup/databases/';
  327. self.upload_file(filename);
  328. endDate = time.strftime('%Y/%m/%d %X', time.localtime())
  329. outTime = time.time() - startTime
  330. pid = sql.table('databases').where('name=?', (name,)).getField('id');
  331. sql.table('backup').add('type,name,pid,filename,addtime,size',
  332. (1, os.path.basename(filename), pid, 'txcos', endDate, os.path.getsize(filename)))
  333. log = "数据库[" + name + "]已成功备份到腾讯云COS,用时[" + str(round(outTime, 2)) + "]秒";
  334. public.WriteLog('计划任务', log)
  335. print("★[" + endDate + "] " + log)
  336. print("|---保留最新的[" + count + "]份备份")
  337. print("|---文件名:" + os.path.basename(filename))
  338. # 清理本地文件
  339. public.ExecShell("rm -f " + filename)
  340. # 清理多余备份
  341. backups = sql.table('backup').where('type=? and pid=? and filename=?', ('1', pid,'txcos')).field('id,name,filename').select();
  342. num = len(backups) - int(count)
  343. if num > 0:
  344. for backup in backups:
  345. if os.path.exists(backup['filename']):
  346. public.ExecShell("rm -f " + backup['filename']);
  347. self.delete_file(self.__bucket_path + backup['name']);
  348. sql.table('backup').where('id=?', (backup['id'],)).delete();
  349. num -= 1;
  350. print("|---已清理过期备份文件:" + backup['name'])
  351. if num < 1: break;
  352. return None
  353. # 备份指定目录
  354. def backupPath(self, path, count):
  355. if not self.__oss:
  356. self.set_cos()
  357. if not self.__oss:
  358. return False
  359. sql = db.Sql();
  360. startTime = time.time();
  361. if path[-1:] == '/': path = path[:-1]
  362. name = os.path.basename(path)
  363. backup_path = sql.table('config').where("id=?", (1,)).getField('backup_path') + '/path';
  364. if not os.path.exists(backup_path): os.makedirs(backup_path);
  365. filename = backup_path + "/Path_" + name + "_" + time.strftime('%Y%m%d_%H%M%S', time.localtime()) + '.tar.gz'
  366. os.system(
  367. "cd " + os.path.dirname(path) + " && tar zcvf '" + filename + "' '" + os.path.basename(path) + "'" + self.__exclude + " > /dev/null")
  368. endDate = time.strftime('%Y/%m/%d %X', time.localtime())
  369. if not os.path.exists(filename):
  370. log = u"目录[" + path + "]备份失败"
  371. print(u"★[" + endDate + "] " + log)
  372. print(u"----------------------------------------------------------------------------")
  373. return;
  374. # 上传文件 ant修改
  375. #if self.__bucket_path != '': self.__bucket_path = 'path/' + 'backup/path' + '/';
  376. self.__bucket_path = self.__bucket_path + 'backup/path/';
  377. self.upload_file(filename);
  378. outTime = time.time() - startTime
  379. sql.table('backup').add('type,name,pid,filename,addtime,size',
  380. ('2', path, '0', filename, endDate, os.path.getsize(filename)))
  381. log = u"目录[" + path + "]备份成功,用时[" + str(round(outTime, 2)) + "]秒";
  382. public.WriteLog(u'计划任务', log)
  383. print(u"★[" + endDate + "] " + log)
  384. print(u"|---保留最新的[" + count + u"]份备份")
  385. print(u"|---文件名:" + filename)
  386. if self.__exclude: print(u"|---排除规则: " + self.__exclude)
  387. # 清理多余备份
  388. backups = sql.table('backup').where('type=? and pid=? and name=?',('2',0,path)).field('id,filename').select();
  389. # 清理本地文件
  390. if os.path.exists(filename): os.remove(filename)
  391. num = len(backups) - int(count)
  392. if num > 0:
  393. for backup in backups:
  394. if os.path.exists(backup['filename']): os.remove(backup['filename'])
  395. self.delete_file(self.__bucket_path + backup['filename']);
  396. sql.table('backup').where('id=?', (backup['id'],)).delete();
  397. num -= 1;
  398. print(u"|---已清理过期备份文件:" + backup['filename'])
  399. if num < 1: break;
  400. def backupSiteAll(self, save):
  401. if not self.__oss:
  402. self.set_cos()
  403. if not self.__oss:
  404. return False
  405. self.__conn()
  406. sites = public.M('sites').field('name').select()
  407. for site in sites:
  408. self.backupSite(site['name'], save)
  409. def backupDatabaseAll(self, save):
  410. if not self.__oss:
  411. self.set_cos()
  412. if self.__oss:
  413. return False
  414. self.__conn()
  415. databases = public.M('databases').field('name').select()
  416. for database in databases:
  417. self.backupDatabase(database['name'], save)
  418. def set_cos(self):
  419. cfile = 'plugin/txcos/config.conf'
  420. fp = open(cfile, 'r')
  421. keys = fp.read().split('|')
  422. self.__secret_id = keys[0]
  423. self.__secret_key = keys[1]
  424. self.__region = keys[2]
  425. self.__Bucket = keys[3]
  426. self.__bucket_path = self.get_path(keys[4])
  427. try:
  428. config = CosConfig(Region=self.__region, SecretId=self.__secret_id, SecretKey=self.__secret_key, Token=None,Scheme='http', Timeout=1)
  429. self.__oss = CosS3Client(config)
  430. except:
  431. if self.__oss == None:
  432. time.sleep(1)
  433. self.__conn()
  434. return json.dumps(self.__error_msg)
  435. def get_lib(self):
  436. import json
  437. list={
  438. "name":"腾讯云COS",
  439. "type":"计划任务",
  440. "ps":"将网站或数据库打包备份到腾讯云COS对象存储空间,, <a class='link' href='https://portal.qiniu.com/signup?code=3liz7nbopjd5e' target='_blank'>点击申请</a>",
  441. "status":'false',
  442. "opt":"txcos",
  443. "module":"qcloud_cos",
  444. "script":"txcos",
  445. "help":"https://www.bt.cn/bbs/thread-17442-1-1.html",
  446. "SecretId":"SecretId|请输入SecretId|腾讯云COS的SecretId",
  447. "SecretKey":"SecretKey|请输入SecretKey|腾讯云COS SecretKey",
  448. "region":"存储地区|请输入对象存储地区|例如 ap-chengdu",
  449. "Bucket":"存储名称|请输入绑定的存储名称",
  450. "check":["/usr/lib/python2.6/site-packages/qcloud_cos/cos_auth.py","/usr/lib/python2.7/site-packages/qcloud_cos/cos_auth.py"]
  451. }
  452. lib='/www/server/panel/data/libList.conf'
  453. lib_dic = json.loads(public.readFile(lib))
  454. for i in lib_dic:
  455. if list['name'] in i['name']:
  456. return True
  457. else:
  458. pass
  459. lib_dic.append(list)
  460. public.writeFile(lib, json.dumps(lib_dic))
  461. return lib_dic
  462. def get_exclode(self):
  463. tmp_exclude = os.getenv('BT_EXCLUDE')
  464. if not tmp_exclude: return ""
  465. for ex in tmp_exclude.split(','):
  466. self.__exclude += " --exclude=\"" + ex + "\""
  467. self.__exclude += " "
  468. return self.__exclude
  469. #取数据库字符集
  470. def get_database_character(self,db_name):
  471. try:
  472. import panelMysql
  473. tmp = panelMysql.panelMysql().query("show create database `%s`" % db_name.strip())
  474. c_type = str(re.findall("SET\s+([\w\d-]+)\s",tmp[0][1])[0])
  475. c_types = ['utf8','utf-8','gbk','big5','utf8mb4']
  476. if not c_type.lower() in c_types: return 'utf8'
  477. return c_type
  478. except:
  479. return 'utf8'
  480. if __name__ == "__main__":
  481. import json
  482. data = None
  483. q = txcos_main()
  484. type = sys.argv[1]
  485. if type == 'site':
  486. if sys.argv[2] == 'ALL':
  487. q.backupSiteAll(sys.argv[3])
  488. else:
  489. q.backupSite(sys.argv[2], sys.argv[3])
  490. exit()
  491. elif type == 'database':
  492. if sys.argv[2] == 'ALL':
  493. q.backupDatabaseAll(sys.argv[3])
  494. else:
  495. q.backupDatabase(sys.argv[2], sys.argv[3])
  496. exit()
  497. elif type == 'path':
  498. q.backupPath(sys.argv[2], sys.argv[3])
  499. elif type == 'upload':
  500. data = q.upload_file(sys.argv[2])
  501. elif type == 'download':
  502. data = q.download_file(sys.argv[2])
  503. elif type == 'get':
  504. data = q.get_files(sys.argv[2])
  505. elif type == 'list':
  506. data = q.get_list()
  507. elif type == 'delete_file':
  508. data = q.delete_file(sys.argv[2])
  509. elif type == 'lib':
  510. data = q.get_lib()
  511. else:
  512. data = 'ERROR: 参数不正确!'
  513. print(json.dumps(data))