23 changed files with 945 additions and 932 deletions
@ -0,0 +1,860 @@ |
|||||
|
#!/usr/bin/python -OO |
||||
|
# Copyright 2008-2017 The SABnzbd-Team <team@sabnzbd.org> |
||||
|
# |
||||
|
# This program is free software; you can redistribute it and/or |
||||
|
# modify it under the terms of the GNU General Public License |
||||
|
# as published by the Free Software Foundation; either version 2 |
||||
|
# of the License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU General Public License |
||||
|
# along with this program; if not, write to the Free Software |
||||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
|
|
||||
|
""" |
||||
|
sabnzbd.misc - filesystem operations |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
import logging |
||||
|
import urllib.request, urllib.parse, urllib.error |
||||
|
import re |
||||
|
import shutil |
||||
|
import threading |
||||
|
import subprocess |
||||
|
import socket |
||||
|
import time |
||||
|
import datetime |
||||
|
import fnmatch |
||||
|
import stat |
||||
|
from urllib.parse import urlparse |
||||
|
|
||||
|
import sabnzbd |
||||
|
from sabnzbd.decorators import synchronized |
||||
|
from sabnzbd.constants import DEFAULT_PRIORITY, FUTURE_Q_FOLDER, JOB_ADMIN, \ |
||||
|
GIGI, MEBI, DEF_CACHE_LIMIT |
||||
|
from sabnzbd.encoding import ubtou, unicoder, special_fixer, gUTF |
||||
|
|
||||
|
|
||||
|
def get_ext(filename): |
||||
|
""" Return lowercased file extension """ |
||||
|
try: |
||||
|
return os.path.splitext(filename)[1].lower() |
||||
|
except: |
||||
|
return '' |
||||
|
|
||||
|
|
||||
|
def get_filename(path): |
||||
|
""" Return path without the file extension """ |
||||
|
try: |
||||
|
return os.path.split(path)[1] |
||||
|
except: |
||||
|
return '' |
||||
|
|
||||
|
|
||||
|
def is_writable(path): |
||||
|
""" Return True is file is writable (also when non-existent) """ |
||||
|
if os.path.isfile(path): |
||||
|
return bool(os.stat(path).st_mode & stat.S_IWUSR) |
||||
|
else: |
||||
|
return True |
||||
|
|
||||
|
|
||||
|
_DEVICES = ('con', 'prn', 'aux', 'nul', |
||||
|
'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', |
||||
|
'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9') |
||||
|
|
||||
|
def replace_win_devices(name): |
||||
|
''' Remove reserved Windows device names from a name. |
||||
|
aux.txt ==> _aux.txt |
||||
|
txt.aux ==> txt.aux |
||||
|
''' |
||||
|
if name: |
||||
|
lname = name.lower() |
||||
|
for dev in _DEVICES: |
||||
|
if lname == dev or lname.startswith(dev + '.'): |
||||
|
name = '_' + name |
||||
|
break |
||||
|
|
||||
|
# Remove special NTFS filename |
||||
|
if lname.startswith('$mft'): |
||||
|
name = name.replace('$', 'S', 1) |
||||
|
|
||||
|
return name |
||||
|
|
||||
|
|
||||
|
def has_win_device(p): |
||||
|
""" Return True if filename part contains forbidden name |
||||
|
Before and after sanitizing |
||||
|
""" |
||||
|
p = os.path.split(p)[1].lower() |
||||
|
for dev in _DEVICES: |
||||
|
if p == dev or p.startswith(dev + '.') or p.startswith('_' + dev + '.'): |
||||
|
return True |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
if sabnzbd.WIN32: |
||||
|
# the colon should be here too, but we'll handle that separately |
||||
|
CH_ILLEGAL = '\/<>?*|"\t' |
||||
|
CH_LEGAL = '++{}!@#`+' |
||||
|
else: |
||||
|
CH_ILLEGAL = '/' |
||||
|
CH_LEGAL = '+' |
||||
|
|
||||
|
|
||||
|
def sanitize_filename(name): |
||||
|
""" Return filename with illegal chars converted to legal ones |
||||
|
and with the par2 extension always in lowercase |
||||
|
""" |
||||
|
if not name: |
||||
|
return name |
||||
|
illegal = CH_ILLEGAL |
||||
|
legal = CH_LEGAL |
||||
|
|
||||
|
if ':' in name: |
||||
|
if sabnzbd.WIN32: |
||||
|
# Compensate for the odd way par2 on Windows substitutes a colon character |
||||
|
name = name.replace(':', '3A') |
||||
|
elif sabnzbd.DARWIN: |
||||
|
# Compensate for the foolish way par2 on OSX handles a colon character |
||||
|
name = name[name.rfind(':') + 1:] |
||||
|
|
||||
|
if sabnzbd.WIN32 or sabnzbd.cfg.sanitize_safe(): |
||||
|
name = replace_win_devices(name) |
||||
|
|
||||
|
lst = [] |
||||
|
for ch in name.strip(): |
||||
|
if ch in illegal: |
||||
|
ch = legal[illegal.find(ch)] |
||||
|
lst.append(ch) |
||||
|
name = ''.join(lst) |
||||
|
|
||||
|
if not name: |
||||
|
name = 'unknown' |
||||
|
|
||||
|
name, ext = os.path.splitext(name) |
||||
|
lowext = ext.lower() |
||||
|
if lowext == '.par2' and lowext != ext: |
||||
|
ext = lowext |
||||
|
return name + ext |
||||
|
|
||||
|
|
||||
|
def sanitize_foldername(name, limit=True): |
||||
|
""" Return foldername with dodgy chars converted to safe ones |
||||
|
Remove any leading and trailing dot and space characters |
||||
|
""" |
||||
|
if not name: |
||||
|
return name |
||||
|
|
||||
|
illegal = CH_ILLEGAL + ':\x92"' |
||||
|
legal = CH_LEGAL + "-''" |
||||
|
|
||||
|
if sabnzbd.cfg.sanitize_safe(): |
||||
|
# Remove all bad Windows chars too |
||||
|
illegal += r'\/<>?*|":' |
||||
|
legal += r'++{}!@#`;' |
||||
|
|
||||
|
repl = sabnzbd.cfg.replace_illegal() |
||||
|
lst = [] |
||||
|
for ch in name.strip(): |
||||
|
if ch in illegal: |
||||
|
if repl: |
||||
|
ch = legal[illegal.find(ch)] |
||||
|
lst.append(ch) |
||||
|
else: |
||||
|
lst.append(ch) |
||||
|
name = ''.join(lst) |
||||
|
name = name.strip() |
||||
|
|
||||
|
if sabnzbd.WIN32 or sabnzbd.cfg.sanitize_safe(): |
||||
|
name = replace_win_devices(name) |
||||
|
|
||||
|
maxlen = sabnzbd.cfg.folder_max_length() |
||||
|
if limit and len(name) > maxlen: |
||||
|
name = name[:maxlen] |
||||
|
|
||||
|
# And finally, make sure it doesn't end in a dot |
||||
|
if name != '.' and name != '..': |
||||
|
name = name.rstrip('.') |
||||
|
if not name: |
||||
|
name = 'unknown' |
||||
|
|
||||
|
return name |
||||
|
|
||||
|
|
||||
|
def sanitize_and_trim_path(path): |
||||
|
""" Remove illegal characters and trim element size """ |
||||
|
path = path.strip() |
||||
|
new_path = '' |
||||
|
if sabnzbd.WIN32: |
||||
|
if path.startswith('\\\\?\\UNC\\'): |
||||
|
new_path = '\\\\?\\UNC\\' |
||||
|
path = path[8:] |
||||
|
elif path.startswith('\\\\?\\'): |
||||
|
new_path = '\\\\?\\' |
||||
|
path = path[4:] |
||||
|
|
||||
|
path = path.replace('\\', '/') |
||||
|
parts = path.split('/') |
||||
|
if sabnzbd.WIN32 and len(parts[0]) == 2 and ':' in parts[0]: |
||||
|
new_path += parts[0] + '/' |
||||
|
parts.pop(0) |
||||
|
elif path.startswith('//'): |
||||
|
new_path = '//' |
||||
|
elif path.startswith('/'): |
||||
|
new_path = '/' |
||||
|
for part in parts: |
||||
|
new_path = os.path.join(new_path, sanitize_foldername(part)) |
||||
|
return os.path.abspath(os.path.normpath(new_path)) |
||||
|
|
||||
|
|
||||
|
def sanitize_files_in_folder(folder): |
||||
|
""" Sanitize each file in the folder, return list of new names |
||||
|
""" |
||||
|
lst = [] |
||||
|
for root, _, files in os.walk(folder): |
||||
|
for file_ in files: |
||||
|
path = os.path.join(root, file_) |
||||
|
new_path = os.path.join(root, sanitize_filename(file_)) |
||||
|
if path != new_path: |
||||
|
try: |
||||
|
os.rename(path, new_path) |
||||
|
path = new_path |
||||
|
except: |
||||
|
logging.debug('Cannot rename %s to %s', path, new_path) |
||||
|
lst.append(path) |
||||
|
return lst |
||||
|
|
||||
|
|
||||
|
def is_obfuscated_filename(filename): |
||||
|
""" Check if this file has an extension, if not, it's |
||||
|
probably obfuscated and we don't use it |
||||
|
""" |
||||
|
return (os.path.splitext(filename)[1] == '') |
||||
|
|
||||
|
|
||||
|
def create_all_dirs(path, umask=False): |
||||
|
""" Create all required path elements and set umask on all |
||||
|
Return True if last element could be made or exists |
||||
|
""" |
||||
|
result = True |
||||
|
if sabnzbd.WIN32: |
||||
|
try: |
||||
|
os.makedirs(path) |
||||
|
except: |
||||
|
result = False |
||||
|
else: |
||||
|
lst = [] |
||||
|
lst.extend(path.split('/')) |
||||
|
path = '' |
||||
|
for d in lst: |
||||
|
if d: |
||||
|
path += '/' + d |
||||
|
if not os.path.exists(path): |
||||
|
try: |
||||
|
os.mkdir(path) |
||||
|
result = True |
||||
|
except: |
||||
|
result = False |
||||
|
if umask: |
||||
|
mask = sabnzbd.cfg.umask() |
||||
|
if mask: |
||||
|
try: |
||||
|
os.chmod(path, int(mask, 8) | 0o700) |
||||
|
except: |
||||
|
pass |
||||
|
return result |
||||
|
|
||||
|
|
||||
|
def real_path(loc, path): |
||||
|
""" When 'path' is relative, return normalized join of 'loc' and 'path' |
||||
|
When 'path' is absolute, return normalized path |
||||
|
A path starting with ~ will be located in the user's Home folder |
||||
|
""" |
||||
|
# The Windows part is a bit convoluted because |
||||
|
# os.path.join() doesn't behave the same for all Python versions |
||||
|
if path: |
||||
|
path = path.strip() |
||||
|
else: |
||||
|
path = '' |
||||
|
if path: |
||||
|
if not sabnzbd.WIN32 and path.startswith('~/'): |
||||
|
path = path.replace('~', os.environ.get('HOME', sabnzbd.DIR_HOME), 1) |
||||
|
if sabnzbd.WIN32: |
||||
|
path = path.replace('/', '\\') |
||||
|
if len(path) > 1 and path[0].isalpha() and path[1] == ':': |
||||
|
if len(path) == 2 or path[2] != '\\': |
||||
|
path = path.replace(':', ':\\', 1) |
||||
|
elif path.startswith('\\\\'): |
||||
|
pass |
||||
|
elif path.startswith('\\'): |
||||
|
if len(loc) > 1 and loc[0].isalpha() and loc[1] == ':': |
||||
|
path = loc[:2] + path |
||||
|
else: |
||||
|
path = os.path.join(loc, path) |
||||
|
elif path[0] != '/': |
||||
|
path = os.path.join(loc, path) |
||||
|
|
||||
|
# Always use long-path notation |
||||
|
path = long_path(path) |
||||
|
else: |
||||
|
path = loc |
||||
|
|
||||
|
return os.path.normpath(os.path.abspath(path)) |
||||
|
|
||||
|
|
||||
|
def create_real_path(name, loc, path, umask=False, writable=True): |
||||
|
""" When 'path' is relative, create join of 'loc' and 'path' |
||||
|
When 'path' is absolute, create normalized path |
||||
|
'name' is used for logging. |
||||
|
Optional 'umask' will be applied. |
||||
|
'writable' means that an existing folder should be writable |
||||
|
Returns ('success', 'full path') |
||||
|
""" |
||||
|
if path: |
||||
|
my_dir = real_path(loc, path) |
||||
|
if not os.path.exists(my_dir): |
||||
|
logging.info('%s directory: %s does not exist, try to create it', name, my_dir) |
||||
|
if not create_all_dirs(my_dir, umask): |
||||
|
logging.error(T('Cannot create directory %s'), clip_path(my_dir)) |
||||
|
return (False, my_dir) |
||||
|
|
||||
|
checks = (os.W_OK + os.R_OK) if writable else os.R_OK |
||||
|
if os.access(my_dir, checks): |
||||
|
return (True, my_dir) |
||||
|
else: |
||||
|
logging.error(T('%s directory: %s error accessing'), name, clip_path(my_dir)) |
||||
|
return (False, my_dir) |
||||
|
else: |
||||
|
return (False, "") |
||||
|
|
||||
|
|
||||
|
def is_relative_path(p): |
||||
|
""" Return True if path is relative """ |
||||
|
p = p.replace('\\', '/') |
||||
|
if p and p[0] == '/': |
||||
|
return False |
||||
|
if sabnzbd.WIN32 and p and len(p) > 2: |
||||
|
if p[0].isalpha() and p[1] == ':' and p[2] == '/': |
||||
|
return False |
||||
|
return True |
||||
|
|
||||
|
|
||||
|
def same_file(a, b): |
||||
|
""" Return 0 if A and B have nothing in common |
||||
|
return 1 if A and B are actually the same path |
||||
|
return 2 if B is a subfolder of A |
||||
|
""" |
||||
|
a = os.path.normpath(os.path.abspath(a)) |
||||
|
b = os.path.normpath(os.path.abspath(b)) |
||||
|
if sabnzbd.WIN32 or sabnzbd.DARWIN: |
||||
|
a = a.lower() |
||||
|
b = b.lower() |
||||
|
|
||||
|
if b.startswith(a): |
||||
|
return 2 |
||||
|
if "samefile" in os.path.__dict__: |
||||
|
try: |
||||
|
return int(os.path.samefile(a, b)) |
||||
|
except: |
||||
|
return 0 |
||||
|
else: |
||||
|
return int(a == b) |
||||
|
|
||||
|
|
||||
|
def check_mount(path): |
||||
|
""" Return False if volume isn't mounted on Linux or OSX |
||||
|
Retry 6 times with an interval of 1 sec. |
||||
|
""" |
||||
|
if sabnzbd.DARWIN: |
||||
|
m = re.search(r'^(/Volumes/[^/]+)/', path, re.I) |
||||
|
elif sabnzbd.WIN32: |
||||
|
m = re.search(r'^([a-z]:\\)', path, re.I) |
||||
|
else: |
||||
|
m = re.search(r'^(/(?:mnt|media)/[^/]+)/', path) |
||||
|
|
||||
|
if m: |
||||
|
for n in range(sabnzbd.cfg.wait_ext_drive() or 1): |
||||
|
if os.path.exists(m.group(1)): |
||||
|
return True |
||||
|
logging.debug('Waiting for %s to come online', m.group(1)) |
||||
|
time.sleep(1) |
||||
|
return not m |
||||
|
|
||||
|
|
||||
|
|
||||
|
def safe_fnmatch(f, pattern): |
||||
|
""" fnmatch will fail if the pattern contains any of it's |
||||
|
key characters, like [, ] or !. |
||||
|
""" |
||||
|
try: |
||||
|
return fnmatch.fnmatch(f, pattern) |
||||
|
except re.error: |
||||
|
return False |
||||
|
|
||||
|
|
||||
|
def globber(path, pattern='*'): |
||||
|
""" Return matching base file/folder names in folder `path` """ |
||||
|
# Cannot use glob.glob() because it doesn't support Windows long name notation |
||||
|
if os.path.exists(path): |
||||
|
return [f for f in os.listdir(path) if safe_fnmatch(f, pattern)] |
||||
|
return [] |
||||
|
|
||||
|
|
||||
|
def globber_full(path, pattern='*'): |
||||
|
""" Return matching full file/folder names in folder `path` """ |
||||
|
# Cannot use glob.glob() because it doesn't support Windows long name notation |
||||
|
if os.path.exists(path): |
||||
|
return [os.path.join(path, f) for f in os.listdir(path) if safe_fnmatch(f, pattern)] |
||||
|
return [] |
||||
|
|
||||
|
|
||||
|
def trim_win_path(path): |
||||
|
""" Make sure Windows path stays below 70 by trimming last part """ |
||||
|
if sabnzbd.WIN32 and len(path) > 69: |
||||
|
path, folder = os.path.split(path) |
||||
|
maxlen = 69 - len(path) |
||||
|
if len(folder) > maxlen: |
||||
|
folder = folder[:maxlen] |
||||
|
path = os.path.join(path, folder).rstrip('. ') |
||||
|
return path |
||||
|
|
||||
|
|
||||
|
def fix_unix_encoding(folder): |
||||
|
""" Fix bad name encoding for Unix systems """ |
||||
|
if not sabnzbd.WIN32 and not sabnzbd.DARWIN and gUTF: |
||||
|
for root, dirs, files in os.walk(folder.encode('utf-8')): |
||||
|
for name in files: |
||||
|
new_name = special_fixer(name).encode('utf-8') |
||||
|
if name != new_name: |
||||
|
try: |
||||
|
shutil.move(os.path.join(root, name), os.path.join(root, new_name)) |
||||
|
except: |
||||
|
logging.info('Cannot correct name of %s', os.path.join(root, name)) |
||||
|
|
||||
|
|
||||
|
def make_script_path(script): |
||||
|
""" Return full script path, if any valid script exists, else None """ |
||||
|
s_path = None |
||||
|
path = sabnzbd.cfg.script_dir.get_path() |
||||
|
if path and script: |
||||
|
if script.lower() not in ('none', 'default'): |
||||
|
s_path = os.path.join(path, script) |
||||
|
if not os.path.exists(s_path): |
||||
|
s_path = None |
||||
|
return s_path |
||||
|
|
||||
|
|
||||
|
def get_admin_path(name, future): |
||||
|
""" Return news-style full path to job-admin folder of names job |
||||
|
or else the old cache path |
||||
|
""" |
||||
|
if future: |
||||
|
return os.path.join(sabnzbd.cfg.admin_dir.get_path(), FUTURE_Q_FOLDER) |
||||
|
else: |
||||
|
return os.path.join(os.path.join(sabnzbd.cfg.download_dir.get_path(), name), JOB_ADMIN) |
||||
|
|
||||
|
def starts_with_path(path, prefix): |
||||
|
""" Return True if 'path' starts with 'prefix', |
||||
|
considering case-sensitivity of the file system |
||||
|
""" |
||||
|
if sabnzbd.WIN32: |
||||
|
return clip_path(path).lower().startswith(prefix.lower()) |
||||
|
elif sabnzbd.DARWIN: |
||||
|
return path.lower().startswith(prefix.lower()) |
||||
|
else: |
||||
|
return path.startswith(prefix) |
||||
|
|
||||
|
|
||||
|
def set_chmod(path, permissions, report): |
||||
|
""" Set 'permissions' on 'path', report any errors when 'report' is True """ |
||||
|
try: |
||||
|
logging.debug('Applying permissions %s (octal) to %s', oct(permissions), path) |
||||
|
os.chmod(path, permissions) |
||||
|
except: |
||||
|
lpath = path.lower() |
||||
|
if report and '.appledouble' not in lpath and '.ds_store' not in lpath: |
||||
|
logging.error(T('Cannot change permissions of %s'), clip_path(path)) |
||||
|
logging.info("Traceback: ", exc_info=True) |
||||
|
|
||||
|
|
||||
|
def set_permissions(path, recursive=True): |
||||
|
""" Give folder tree and its files their proper permissions """ |
||||
|
if not sabnzbd.WIN32: |
||||
|
umask = sabnzbd.cfg.umask() |
||||
|
try: |
||||
|
# Make sure that user R+W+X is on |
||||
|
umask = int(umask, 8) | int('0700', 8) |
||||
|
report = True |
||||
|
except ValueError: |
||||
|
# No or no valid permissions |
||||
|
# Use the effective permissions of the session |
||||
|
# Don't report errors (because the system might not support it) |
||||
|
umask = int('0777', 8) & (sabnzbd.ORG_UMASK ^ int('0777', 8)) |
||||
|
report = False |
||||
|
|
||||
|
# Remove X bits for files |
||||
|
umask_file = umask & int('7666', 8) |
||||
|
|
||||
|
if os.path.isdir(path): |
||||
|
if recursive: |
||||
|
# Parse the dir/file tree and set permissions |
||||
|
for root, _dirs, files in os.walk(path): |
||||
|
set_chmod(root, umask, report) |
||||
|
for name in files: |
||||
|
set_chmod(os.path.join(root, name), umask_file, report) |
||||
|
else: |
||||
|
set_chmod(path, umask, report) |
||||
|
else: |
||||
|
set_chmod(path, umask_file, report) |
||||
|
|
||||
|
|
||||
|
def clip_path(path): |
||||
|
r""" Remove \\?\ or \\?\UNC\ prefix from Windows path """ |
||||
|
if sabnzbd.WIN32 and path and '?' in path: |
||||
|
path = path.replace('\\\\?\\UNC\\', '\\\\', 1).replace('\\\\?\\', '', 1) |
||||
|
return path |
||||
|
|
||||
|
|
||||
|
def long_path(path): |
||||
|
""" For Windows, convert to long style path; others, return same path """ |
||||
|
if sabnzbd.WIN32 and path and not path.startswith('\\\\?\\'): |
||||
|
if path.startswith('\\\\'): |
||||
|
# Special form for UNC paths |
||||
|
path = path.replace('\\\\', '\\\\?\\UNC\\', 1) |
||||
|
else: |
||||
|
# Normal form for local paths |
||||
|
path = '\\\\?\\' + path |
||||
|
return path |
||||
|
|
||||
|
|
||||
|
############################################################################## |
||||
|
# Locked directory operations to avoid problems with simultaneous add/remove |
||||
|
############################################################################## |
||||
|
DIR_LOCK = threading.RLock() |
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def get_unique_path(dirpath, n=0, create_dir=True): |
||||
|
""" Determine a unique folder or filename """ |
||||
|
|
||||
|
if not check_mount(dirpath): |
||||
|
return dirpath |
||||
|
|
||||
|
path = dirpath |
||||
|
if n: |
||||
|
path = "%s.%s" % (dirpath, n) |
||||
|
|
||||
|
if not os.path.exists(path): |
||||
|
if create_dir: |
||||
|
return create_dirs(path) |
||||
|
else: |
||||
|
return path |
||||
|
else: |
||||
|
return get_unique_path(dirpath, n=n + 1, create_dir=create_dir) |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def get_unique_filename(path): |
||||
|
""" Check if path is unique. |
||||
|
If not, add number like: "/path/name.NUM.ext". |
||||
|
""" |
||||
|
num = 1 |
||||
|
new_path, fname = os.path.split(path) |
||||
|
name, ext = os.path.splitext(fname) |
||||
|
while os.path.exists(path): |
||||
|
fname = "%s.%d%s" % (name, num, ext) |
||||
|
num += 1 |
||||
|
path = os.path.join(new_path, fname) |
||||
|
return path |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def create_dirs(dirpath): |
||||
|
""" Create directory tree, obeying permissions """ |
||||
|
if not os.path.exists(dirpath): |
||||
|
logging.info('Creating directories: %s', dirpath) |
||||
|
if not create_all_dirs(dirpath, True): |
||||
|
logging.error(T('Failed making (%s)'), clip_path(dirpath)) |
||||
|
return None |
||||
|
return dirpath |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def move_to_path(path, new_path): |
||||
|
""" Move a file to a new path, optionally give unique filename |
||||
|
Return (ok, new_path) |
||||
|
""" |
||||
|
ok = True |
||||
|
overwrite = sabnzbd.cfg.overwrite_files() |
||||
|
new_path = os.path.abspath(new_path) |
||||
|
if overwrite and os.path.exists(new_path): |
||||
|
try: |
||||
|
os.remove(new_path) |
||||
|
except: |
||||
|
overwrite = False |
||||
|
if not overwrite: |
||||
|
new_path = get_unique_filename(new_path) |
||||
|
|
||||
|
if new_path: |
||||
|
logging.debug("Moving (overwrite: %s) %s => %s", overwrite, path, new_path) |
||||
|
try: |
||||
|
# First try cheap rename |
||||
|
renamer(path, new_path) |
||||
|
except: |
||||
|
# Cannot rename, try copying |
||||
|
logging.debug("File could not be renamed, trying copying: %s", path) |
||||
|
try: |
||||
|
if not os.path.exists(os.path.dirname(new_path)): |
||||
|
create_dirs(os.path.dirname(new_path)) |
||||
|
shutil.copyfile(path, new_path) |
||||
|
os.remove(path) |
||||
|
except: |
||||
|
# Check if the old-file actually exists (possible delete-delays) |
||||
|
if not os.path.exists(path): |
||||
|
logging.debug("File not moved, original path gone: %s", path) |
||||
|
return True, None |
||||
|
if not (sabnzbd.cfg.marker_file() and sabnzbd.cfg.marker_file() in path): |
||||
|
logging.error(T('Failed moving %s to %s'), clip_path(path), clip_path(new_path)) |
||||
|
logging.info("Traceback: ", exc_info=True) |
||||
|
ok = False |
||||
|
return ok, new_path |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def cleanup_empty_directories(path): |
||||
|
""" Remove all empty folders inside (and including) 'path' """ |
||||
|
path = os.path.normpath(path) |
||||
|
while 1: |
||||
|
repeat = False |
||||
|
for root, dirs, files in os.walk(path, topdown=False): |
||||
|
if not dirs and not files and root != path: |
||||
|
try: |
||||
|
remove_dir(root) |
||||
|
repeat = True |
||||
|
except: |
||||
|
pass |
||||
|
if not repeat: |
||||
|
break |
||||
|
try: |
||||
|
remove_dir(path) |
||||
|
except: |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def get_filepath(path, nzo, filename): |
||||
|
""" Create unique filepath """ |
||||
|
# This procedure is only used by the Assembler thread |
||||
|
# It does no umask setting |
||||
|
# It uses the dir_lock for the (rare) case that the |
||||
|
# download_dir is equal to the complete_dir. |
||||
|
dName = nzo.work_name |
||||
|
if not nzo.created: |
||||
|
for n in range(200): |
||||
|
dName = dirname |
||||
|
if n: |
||||
|
dName += '.' + str(n) |
||||
|
try: |
||||
|
os.mkdir(os.path.join(path, dName)) |
||||
|
break |
||||
|
except: |
||||
|
pass |
||||
|
nzo.work_name = dName |
||||
|
nzo.created = True |
||||
|
|
||||
|
fPath = os.path.join(os.path.join(path, dName), filename) |
||||
|
fPath, ext = os.path.splitext(fPath) |
||||
|
n = 0 |
||||
|
while True: |
||||
|
if n: |
||||
|
fullPath = "%s.%d%s" % (fPath, n, ext) |
||||
|
else: |
||||
|
fullPath = fPath + ext |
||||
|
if os.path.exists(fullPath): |
||||
|
n = n + 1 |
||||
|
else: |
||||
|
break |
||||
|
|
||||
|
return fullPath |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def renamer(old, new): |
||||
|
""" Rename file/folder with retries for Win32 """ |
||||
|
# Sanitize last part of new name |
||||
|
path, name = os.path.split(new) |
||||
|
# Use the more stringent folder rename to end up with a nicer name, |
||||
|
# but do not trim size |
||||
|
new = os.path.join(path, sanitize_foldername(name, False)) |
||||
|
|
||||
|
logging.debug('Renaming "%s" to "%s"', old, new) |
||||
|
if sabnzbd.WIN32: |
||||
|
retries = 15 |
||||
|
while retries > 0: |
||||
|
# First we try 3 times with os.rename |
||||
|
if retries > 12: |
||||
|
try: |
||||
|
os.rename(old, new) |
||||
|
return |
||||
|
except: |
||||
|
retries -= 1 |
||||
|
time.sleep(3) |
||||
|
continue |
||||
|
|
||||
|
# Now we try the back-up method |
||||
|
logging.debug('Could not rename, trying move for %s to %s', old, new) |
||||
|
try: |
||||
|
shutil.move(old, new) |
||||
|
return |
||||
|
except WindowsError as err: |
||||
|
logging.debug('Error renaming "%s" to "%s" <%s>', old, new, err) |
||||
|
if err[0] == 32: |
||||
|
logging.debug('Retry rename %s to %s', old, new) |
||||
|
retries -= 1 |
||||
|
else: |
||||
|
raise WindowsError(err) |
||||
|
time.sleep(3) |
||||
|
raise WindowsError(err) |
||||
|
else: |
||||
|
shutil.move(old, new) |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def remove_dir(path): |
||||
|
""" Remove directory with retries for Win32 """ |
||||
|
logging.debug('Removing dir %s', path) |
||||
|
if sabnzbd.WIN32: |
||||
|
retries = 15 |
||||
|
while retries > 0: |
||||
|
try: |
||||
|
os.rmdir(path) |
||||
|
return |
||||
|
except WindowsError as err: |
||||
|
if err[0] == 32: |
||||
|
logging.debug('Retry delete %s', path) |
||||
|
retries -= 1 |
||||
|
else: |
||||
|
raise WindowsError(err) |
||||
|
time.sleep(3) |
||||
|
raise WindowsError(err) |
||||
|
else: |
||||
|
os.rmdir(path) |
||||
|
|
||||
|
|
||||
|
@synchronized(DIR_LOCK) |
||||
|
def remove_all(path, pattern='*', keep_folder=False, recursive=False): |
||||
|
""" Remove folder and all its content (optionally recursive) """ |
||||
|
if os.path.exists(path): |
||||
|
files = globber_full(path, pattern) |
||||
|
if pattern == '*' and not sabnzbd.WIN32: |
||||
|
files.extend(globber_full(path, '.*')) |
||||
|
|
||||
|
for f in files: |
||||
|
if os.path.isfile(f): |
||||
|
try: |
||||
|
logging.debug('Removing file %s', f) |
||||
|
os.remove(f) |
||||
|
except: |
||||
|
logging.info('Cannot remove file %s', f) |
||||
|
elif recursive: |
||||
|
remove_all(f, pattern, False, True) |
||||
|
if not keep_folder: |
||||
|
try: |
||||
|
logging.debug('Removing dir %s', path) |
||||
|
os.rmdir(path) |
||||
|
except: |
||||
|
logging.info('Cannot remove folder %s', path) |
||||
|
|
||||
|
############################################################################## |
||||
|
# Diskfree |
||||
|
############################################################################## |
||||
|
def find_dir(p): |
||||
|
""" Return first folder level that exists in this path """ |
||||
|
x = 'x' |
||||
|
while x and not os.path.exists(p): |
||||
|
p, x = os.path.split(p) |
||||
|
return p |
||||
|
|
||||
|
|
||||
|
if sabnzbd.WIN32: |
||||
|
# windows diskfree |
||||
|
try: |
||||
|
# Careful here, because win32api test hasn't been done yet! |
||||
|
import win32api |
||||
|
except: |
||||
|
pass |
||||
|
|
||||
|
def diskspace_base(_dir): |
||||
|
""" Return amount of free and used diskspace in GBytes """ |
||||
|
_dir = find_dir(_dir) |
||||
|
try: |
||||
|
available, disk_size, total_free = win32api.GetDiskFreeSpaceEx(_dir) |
||||
|
return disk_size / GIGI, available / GIGI |
||||
|
except: |
||||
|
return 0.0, 0.0 |
||||
|
|
||||
|
else: |
||||
|
try: |
||||
|
os.statvfs |
||||
|
# posix diskfree |
||||
|
def diskspace_base(_dir): |
||||
|
""" Return amount of free and used diskspace in GBytes """ |
||||
|
_dir = find_dir(_dir) |
||||
|
try: |
||||
|
s = os.statvfs(_dir) |
||||
|
if s.f_blocks < 0: |
||||
|
disk_size = float(sys.maxsize) * float(s.f_frsize) |
||||
|
else: |
||||
|
disk_size = float(s.f_blocks) * float(s.f_frsize) |
||||
|
if s.f_bavail < 0: |
||||
|
available = float(sys.maxsize) * float(s.f_frsize) |
||||
|
else: |
||||
|
available = float(s.f_bavail) * float(s.f_frsize) |
||||
|
return disk_size / GIGI, available / GIGI |
||||
|
except: |
||||
|
return 0.0, 0.0 |
||||
|
except ImportError: |
||||
|
def diskspace_base(_dir): |
||||
|
return 20.0, 10.0 |
||||
|
|
||||
|
|
||||
|
# Store all results to speed things up |
||||
|
__DIRS_CHECKED = [] |
||||
|
__DISKS_SAME = None |
||||
|
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []} |
||||
|
__LAST_DISK_CALL = 0 |
||||
|
|
||||
|
def diskspace(force=False): |
||||
|
""" Wrapper to cache results """ |
||||
|
global __DIRS_CHECKED, __DISKS_SAME, __LAST_DISK_RESULT, __LAST_DISK_CALL |
||||
|
|
||||
|
# Reset everything when folders changed |
||||
|
dirs_to_check = [sabnzbd.cfg.download_dir.get_path(), sabnzbd.cfg.complete_dir.get_path()] |
||||
|
if __DIRS_CHECKED != dirs_to_check: |
||||
|
__DIRS_CHECKED = dirs_to_check |
||||
|
__DISKS_SAME = None |
||||
|
__LAST_DISK_RESULT = {'download_dir': [], 'complete_dir': []} |
||||
|
__LAST_DISK_CALL = 0 |
||||
|
|
||||
|
# When forced, ignore any cache to avoid problems in UI |
||||
|
if force: |
||||
|
__LAST_DISK_CALL = 0 |
||||
|
|
||||
|
# Check against cache |
||||
|
if time.time() > __LAST_DISK_CALL + 10.0: |
||||
|
# Same disk? Then copy-paste |
||||
|
__LAST_DISK_RESULT['download_dir'] = diskspace_base(sabnzbd.cfg.download_dir.get_path()) |
||||
|
__LAST_DISK_RESULT['complete_dir'] = __LAST_DISK_RESULT['download_dir'] if __DISKS_SAME else diskspace_base(sabnzbd.cfg.complete_dir.get_path()) |
||||
|
__LAST_DISK_CALL = time.time() |
||||
|
|
||||
|
# Do we know if it's same disk? |
||||
|
if __DISKS_SAME is None: |
||||
|
__DISKS_SAME = (__LAST_DISK_RESULT['download_dir'] == __LAST_DISK_RESULT['complete_dir']) |
||||
|
|
||||
|
return __LAST_DISK_RESULT |
Loading…
Reference in new issue