"""
Files and directories
=====================
This module provides high-level tools for managing files and
directories.
"""
from __future__ import with_statement
from pipes import quote
from tempfile import mkstemp
from urlparse import urlparse
import hashlib
import os
from fabric.api import hide, put, run, settings
from fabtools.files import (
group as _group,
is_file,
is_dir,
md5sum,
mode as _mode,
owner as _owner,
umask,
)
from fabtools.utils import run_as_root
import fabtools.files
BLOCKSIZE = 2 ** 20 # 1MB
[docs]def directory(path, use_sudo=False, owner='', group='', mode=''):
"""
Require a directory to exist.
::
from fabtools import require
require.directory('/tmp/mydir', owner='alice', use_sudo=True)
.. note:: This function can be accessed directly from the
``fabtools.require`` module for convenience.
"""
func = use_sudo and run_as_root or run
if not is_dir(path):
func('mkdir -p "%(path)s"' % locals())
# Ensure correct owner
if (owner and _owner(path, use_sudo) != owner) or \
(group and _group(path, use_sudo) != group):
func('chown %(owner)s:%(group)s "%(path)s"' % locals())
# Ensure correct mode
if mode and _mode(path, use_sudo) != mode:
func('chmod %(mode)s "%(path)s"' % locals())
[docs]def directories(path_list, use_sudo=False, owner='', group='', mode=''):
"""
Require a list of directories to exist.
::
from fabtools import require
dirs=[
'/tmp/mydir',
'/tmp/mydear',
'/tmp/my/dir'
]
require.directories(dirs, owner='alice', mode='750')
.. note:: This function can be accessed directly from the
``fabtools.require`` module for convenience.
"""
for path in path_list:
directory(path, use_sudo, owner, group, mode)
[docs]def file(path=None, contents=None, source=None, url=None, md5=None,
use_sudo=False, owner=None, group='', mode=None, verify_remote=True,
temp_dir='/tmp'):
"""
Require a file to exist and have specific contents and properties.
You can provide either:
- *contents*: the required contents of the file::
from fabtools import require
require.file('/tmp/hello.txt', contents='Hello, world')
- *source*: the local path of a file to upload::
from fabtools import require
require.file('/tmp/hello.txt', source='files/hello.txt')
- *url*: the URL of a file to download (*path* is then optional)::
from fabric.api import cd
from fabtools import require
with cd('tmp'):
require.file(url='http://example.com/files/hello.txt')
If *verify_remote* is ``True`` (the default), then an MD5 comparison
will be used to check whether the remote file is the same as the
source. If this is ``False``, the file will be assumed to be the
same if it is present. This is useful for very large files, where
generating an MD5 sum may take a while.
When providing either the *contents* or the *source* parameter, Fabric's
``put`` function will be used to upload the file to the remote host.
When ``use_sudo`` is ``True``, the file will first be uploaded to a temporary
directory, then moved to its final location. The default temporary
directory is ``/tmp``, but can be overridden with the *temp_dir* parameter.
If *temp_dir* is an empty string, then the user's home directory will
be used.
If `use_sudo` is `True`, then the remote file will be owned by root,
and its mode will reflect root's default *umask*. The optional *owner*,
*group* and *mode* parameters can be used to override these properties.
.. note:: This function can be accessed directly from the
``fabtools.require`` module for convenience.
"""
func = use_sudo and run_as_root or run
# 1) Only a path is given
if path and not (contents or source or url):
assert path
if not is_file(path):
func('touch "%(path)s"' % locals())
# 2) A URL is specified (path is optional)
elif url:
if not path:
path = os.path.basename(urlparse(url).path)
if not is_file(path) or md5 and md5sum(path) != md5:
func('wget --progress=dot:mega %(url)s -O %(path)s' % locals())
# 3) A local filename, or a content string, is specified
else:
if source:
assert not contents
t = None
else:
fd, source = mkstemp()
t = os.fdopen(fd, 'w')
t.write(contents)
t.close()
if verify_remote:
# Avoid reading the whole file into memory at once
digest = hashlib.md5()
f = open(source, 'rb')
try:
while True:
d = f.read(BLOCKSIZE)
if not d:
break
digest.update(d)
finally:
f.close()
else:
digest = None
if (not is_file(path, use_sudo=use_sudo) or
(verify_remote and
md5sum(path, use_sudo=use_sudo) != digest.hexdigest())):
with settings(hide('running')):
put(source, path, use_sudo=use_sudo, temp_dir=temp_dir)
if t is not None:
os.unlink(source)
# Ensure correct owner
if use_sudo and owner is None:
owner = 'root'
if (owner and _owner(path, use_sudo) != owner) or \
(group and _group(path, use_sudo) != group):
func('chown %(owner)s:%(group)s "%(path)s"' % locals())
# Ensure correct mode
if use_sudo and mode is None:
mode = oct(0666 & ~int(umask(use_sudo=True), base=8))
if mode and _mode(path, use_sudo) != mode:
func('chmod %(mode)s "%(path)s"' % locals())
[docs]def template_file(path=None, template_contents=None, template_source=None, context=None, **kwargs):
"""
Require a file whose contents is defined by a template.
"""
if template_contents is None:
with open(template_source) as template_file:
template_contents = template_file.read()
if context is None:
context = {}
file(path=path, contents=template_contents % context, **kwargs)
[docs]def temporary_directory(template=None):
"""
Require a temporary directory.
The directory is created using the ``mktemp`` command. It will
be created in ``/tmp``, unless the ``TMPDIR`` environment variable
is set to another location. ::
from fabtools.require.files import temporary_directory
tmp_dir = temporary_directory()
You can choose a specific location and name template for the
temporary directory: ::
from fabtools.require.files import temporary_directory
tmp_dir = temporary_directory('/var/tmp/temp.XXXXXX')
You can also call this function as a context manager. In this case,
the directory and its contents will be automatically deleted when
exiting the block: ::
from pipes import quote
from posixpath import join
from fabtools.require.files import temporary_directory
with temporary_directory() as tmp_dir:
path = join(tmp_dir, 'foo')
run('touch %s' % quote(path))
"""
options = ['--directory']
if template:
options.append(template)
options = ' '.join(options)
with hide('running', 'stdout'):
path = run('mktemp %s' % options)
return TemporaryDirectory(path)
class TemporaryDirectory(str):
def __enter__(self):
return self
def __exit__(self, type, value, tb):
run('rm -rf %s' % quote(self))