+def safe_unlink(file):
+ """Moves a file/dir to a backup location."""
+ if not os.path.lexists(file):
+ return None
+ prefix = "%s.bak" % file
+ name = None
+ for i in itertools.count():
+ name = "%s.%d" % (prefix, i)
+ if not os.path.lexists(name):
+ break
+ os.rename(file, name)
+ return name
+
+def soft_unlink(file):
+ """Unlink a file, but don't complain if it doesn't exist."""
+ try:
+ os.unlink(file)
+ except OSError:
+ pass
+
+def makedirs(path):
+ """
+ Create a directory path (a la ``mkdir -p`` or ``os.makedirs``),
+ but don't complain if it already exists.
+ """
+ try:
+ os.makedirs(path)
+ except OSError as exc:
+ if exc.errno == errno.EEXIST:
+ pass
+ else:
+ raise
+
+def fetch(host, path, subpath, post=None):
+ try:
+ # XXX: Should use urllib instead
+ h = httplib.HTTPConnection(host)
+ fullpath = path.rstrip("/") + "/" + subpath.lstrip("/") # to be lenient about input we accept
+ if post:
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ logging.info("POST request to http://%s%s", host, fullpath)
+ logging.debug("POST contents:\n" + urllib.urlencode(post))
+ h.request("POST", fullpath, urllib.urlencode(post), headers)
+ else:
+ logging.info("GET request to http://%s%s", host, fullpath)
+ h.request("GET", fullpath)
+ r = h.getresponse()
+ data = r.read()
+ h.close()
+ return data
+ except socket.gaierror as e:
+ if e.errno == socket.EAI_NONAME:
+ raise DNSError(host)
+ else:
+ raise
+
+def mixed_newlines(filename):
+ """Returns ``True`` if ``filename`` has mixed newlines."""
+ f = open(filename, "U") # requires universal newline support
+ f.read()
+ ret = isinstance(f.newlines, tuple)
+ f.close() # just to be safe
+ return ret
+
+def disk_usage(dir=None, excluded_dir=".git"):
+ """
+ Recursively determines the disk usage of a directory, excluding
+ .git directories. Value is in bytes. If ``dir`` is omitted, the
+ current working directory is assumed.
+ """
+ if dir is None: dir = os.getcwd()
+ sum_sizes = 0
+ for root, _, files in os.walk(dir):
+ for name in files:
+ if not os.path.join(root, name).startswith(os.path.join(dir, excluded_dir)):
+ file = os.path.join(root, name)
+ try:
+ if os.path.islink(file): continue
+ sum_sizes += os.path.getsize(file)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ logging.warning("%s disappeared before we could stat", file)
+ else:
+ raise
+ return sum_sizes
+
+def random_key(length=30):
+ """Generates a random alphanumeric key of ``length`` size."""
+ return ''.join(random.choice(string.letters + string.digits) for i in xrange(length))
+
+def truncate(version):
+ """Truncates the Scripts specific version number."""
+ return str(version).partition('-scripts')[0]
+
+def init_wizard_dir():
+ """
+ Generates a .wizard directory and initializes it with some common
+ files. This operation is idempotent.
+ """
+ # no harm in doing this repeatedly
+ wizard_dir = ".wizard"
+ if not os.path.isdir(wizard_dir):
+ os.mkdir(wizard_dir)
+ open(os.path.join(wizard_dir, ".htaccess"), "w").write("Deny from all\n")
+ open(os.path.join(wizard_dir, ".gitignore"), "w").write("*\n")
+