]> scripts.mit.edu Git - wizard.git/commitdiff
Move wizard.scripts module to plugins, added hooks accordingly.
authorEdward Z. Yang <ezyang@mit.edu>
Tue, 8 Jun 2010 06:06:34 +0000 (23:06 -0700)
committerEdward Z. Yang <ezyang@mit.edu>
Tue, 8 Jun 2010 06:06:34 +0000 (23:06 -0700)
Renamed the following functions:

    * scripts.fill_url -> deploy.web (now returns list of candidate
      urls and doesn't accept boolean old_style parameter)
    * scripts.get_disk_usage -> util.disk_usage (disk_usage now
      returns values in bytes, not kibibytes)

Scripts specific logic was refactored to scripts plugin.  Scripts
plugin got a setup.py file.

Created these plugin entry points:

    * wizard.deploy.web
    * wizard.user.quota

Added new functions:

    * user.quota (wizard.user module is new)

Fixed bugs:

    * git status on later versions of Git doesn't vary exit code;
      use diff-files and diff-index instead.

Restructured the deploy.Deployment.url code to run the generator of URLs
given to it by deploy.web.

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
28 files changed:
doc/index.rst
doc/module/wizard.deploy.rst
doc/module/wizard.scripts.rst [deleted file]
doc/module/wizard.util.rst
doc/plugin.rst
plugins/scripts/.gitignore [new file with mode: 0644]
plugins/scripts/setup.py [new file with mode: 0644]
plugins/scripts/wizard/scripts.py [moved from wizard/scripts.py with 52% similarity]
pull.sh
tests/mediawiki-crlf-upgrade-test.sh
tests/mediawiki-install
wizard/app/__init__.py
wizard/app/wordpress.py
wizard/command/mass_upgrade.py
wizard/command/quota.py
wizard/command/upgrade.py
wizard/deploy.py
wizard/install/__init__.py
wizard/tests/disk_usage_test/dont_ignore_me/file_1 [moved from wizard/tests/scripts_test/dont_ignore_me/file_1 with 100% similarity]
wizard/tests/disk_usage_test/file_2 [moved from wizard/tests/scripts_test/file_2 with 100% similarity]
wizard/tests/disk_usage_test/file_4 [moved from wizard/tests/scripts_test/file_4 with 100% similarity]
wizard/tests/disk_usage_test/ignore_me/file_16 [moved from wizard/tests/scripts_test/ignore_me/file_16 with 100% similarity]
wizard/tests/disk_usage_test/ignore_me/file_8 [moved from wizard/tests/scripts_test/ignore_me/file_8 with 100% similarity]
wizard/tests/disk_usage_test/ignore_me/ignore_me_too/file_32 [moved from wizard/tests/scripts_test/ignore_me/ignore_me_too/file_32 with 100% similarity]
wizard/tests/scripts_test.py [deleted file]
wizard/tests/util_test.py
wizard/user.py [new file with mode: 0644]
wizard/util.py

index ff17ed6a24b45c675f5b40fd474c2271c8dfcc04..4437208aa3a7c9158773182d099469619386673d 100644 (file)
@@ -79,7 +79,6 @@ Modules
     module/wizard.merge
     module/wizard.prompt
     module/wizard.resolve
-    module/wizard.scripts
     module/wizard.shell
     module/wizard.sql
     module/wizard.sset
index c56aabd9008ab4914e6efbaeec8df5bbcc27a03a..bbfd22b89334ff14725075bcbc88c4f5eeac7c77 100644 (file)
@@ -19,6 +19,7 @@ Functions
 .. autofunction:: get_install_lines
 .. autofunction:: parse_install_lines
 .. autofunction:: chdir_to_location
+.. autofunction:: web
 
 Exceptions
 ----------
diff --git a/doc/module/wizard.scripts.rst b/doc/module/wizard.scripts.rst
deleted file mode 100644 (file)
index 2433e99..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-:mod:`wizard.scripts`
-=====================
-
-.. automodule:: wizard.scripts
-
-Functions
----------
-.. autofunction:: fill_url
-.. autofunction:: get_quota_usage_and_limit
-.. autofunction:: get_disk_usage
-
-Exceptions
-----------
-.. autoexception:: QuotaParseError
-
index b68164707e0af966c295c79eb46754779df65da9..d8aa162d3ab40cd1546ba5174d5246a1046d71b1 100644 (file)
@@ -35,6 +35,7 @@ Functions
 .. autofunction:: mixed_newlines
 .. autofunction:: random_key
 .. autofunction:: makedirs
+.. autofunction:: disk_usage
 
 Exceptions
 ----------
index 86d95240171597f4c888342277ab5f6e2318d206..c30fc5df4deba002a1ab9133c45039e2ea985b53 100644 (file)
@@ -43,7 +43,7 @@ For more information on how to create an application, check
 
 .. todo::
 
-    The below plugins are not implemented by Wizard yet.
+    Plugins marked with prototype todo messages are not complete yet.
 
 ``wizard.strategy``
 -------------------
@@ -59,6 +59,16 @@ able to be determined from an application's configuration files.
 
 .. todo:: Prototype
 
+``wizard.deploy.web``
+---------------------
+
+Used to fill in a user's web URL if it is not able to be determined from
+an application's configuration files.  Plugin should be a function that
+takes a single required argument ``dir``, which is the directory to
+determint the web URL(s) for, and return a list or generator of
+:class:`urlparse.ParseResult` objects or URL strings of possible web locations
+for an application.
+
 ``wizard.user.email``
 ---------------------
 
@@ -87,3 +97,15 @@ resolve the username from a UID.  Use this hook to implement an
 alternative ``passwd`` lookup.
 
 .. todo:: Prototype
+
+``wizard.user.quota``
+---------------------
+
+Plugin for :func:`wizard.user.quota`.  Wizard has safeguards for
+avoiding exceeding user quotas when an automated upgrade is being
+performed.  Unfortunately, methods for enforcing quotas are frequently
+highly system dependent.  Use this hook to implement quota usage and
+limit reporting.  Plugin should be a function that takes a single
+required argument ``dir``, which is the directory to determine the quota
+for, and return a tuple ``(quota usage, quota limit)`` or ``(0, None)``
+if it could not determine quota.
diff --git a/plugins/scripts/.gitignore b/plugins/scripts/.gitignore
new file mode 100644 (file)
index 0000000..4537a12
--- /dev/null
@@ -0,0 +1 @@
+wizard_scripts.egg-info
diff --git a/plugins/scripts/setup.py b/plugins/scripts/setup.py
new file mode 100644 (file)
index 0000000..a61c2bb
--- /dev/null
@@ -0,0 +1,16 @@
+import setuptools
+
+setuptools.setup(
+    name = 'wizard-scripts',
+    version = '0.1.dev',
+    author = 'The Scripts Team',
+    author_email = 'scripts-team@mit.edu',
+    description = ('Customizations to Wizard for scripts.mit.edu'),
+    license = 'MIT',
+    url = 'http://scripts.mit.edu/',
+    packages = setuptools.find_packages(),
+    entry_points = {
+        'wizard.user.quota': 'scripts = wizard.scripts:user_quota',
+        'wizard.deploy.web': 'scripts = wizard.scripts:deploy_web',
+    }
+)
similarity index 52%
rename from wizard/scripts.py
rename to plugins/scripts/wizard/scripts.py
index 9092f268d0be2a96cbe33344fba7c701e8bde536..517ae7b78a2f2d420f7ae0982f2114b52d8f260c 100644 (file)
@@ -13,45 +13,22 @@ import errno
 import wizard
 from wizard import shell, util
 
-def fill_url(dir, url=None, old_style=False):
-    """
-    Attempts to determine the URL a directory would be web-accessible at.
-    If ``url`` is specified, automatically use it.
-    """
-    if url:
-        return url
-
-    # hook hook
-
+def deploy_web()
     # try the directory
     homedir, _, web_path = dir.partition("/web_scripts")
     if web_path:
-        if old_style:
-            return urlparse.ParseResult(
-                    "http",
-                    "scripts.mit.edu",
-                    "/~" + util.get_dir_owner(homedir) + web_path.rstrip('/'),
-                    "", "", "")
-        else:
-            return urlparse.ParseResult(
-                    "http",
-                    util.get_dir_owner(homedir) + ".scripts.mit.edu",
-                    web_path.rstrip('/'),
-                    "", "", "")
-
-    # try the environment
-    host = os.getenv("WIZARD_WEB_HOST")
-    path = os.getenv("WIZARD_WEB_PATH")
-    if host is not None and path is not None:
-        return urlparse.ParseResult(
+        yield urlparse.ParseResult(
                 "http",
-                host,
-                path.rstrip('/'),
+                util.get_dir_owner(homedir) + ".scripts.mit.edu",
+                web_path.rstrip('/'),
+                "", "", "")
+        yield urlparse.ParseResult(
+                "http",
+                "scripts.mit.edu",
+                "/~" + util.get_dir_owner(homedir) + web_path.rstrip('/'),
                 "", "", "")
 
-    return None
-
-def get_quota_usage_and_limit(dir=None):
+def user_quota(dir=None):
     """
     Returns a tuple (quota usage, quota limit).  Works only for scripts
     servers.  Values are in KiB.  Returns ``(0, None)`` if we couldn't figure it out.
@@ -60,16 +37,17 @@ def get_quota_usage_and_limit(dir=None):
     # sometimes the volume is busy; so we try several times
     for i in range(0, end + 1):
         try:
-            return _get_quota_usage_and_limit(dir)
+            return _user_quota(dir)
         except QuotaParseError as e:
             if i == end:
                 raise e
             time.sleep(3) # give it a chance to unbusy
     assert False # should not get here
 
-def _get_quota_usage_and_limit(dir=None):
+def _user_quota(dir=None):
     # XXX: The correct way is to implement Python modules implementing
     # bindings for all the appropriate interfaces
+    unknown = (0, None)
     def parse_last_quote(ret):
         return ret.rstrip('\'').rpartition('\'')[2]
     if dir is None:
@@ -78,10 +56,10 @@ def _get_quota_usage_and_limit(dir=None):
     try:
         cell = parse_last_quote(sh.eval("fs", "whichcell", "-path", dir))
     except shell.CallError:
-        return (0, None)
+        return unknown
     except OSError as e:
         if e.errno == errno.ENOENT:
-            return (0, None)
+            return unknown
         raise
     mount = None
     while dir:
@@ -92,18 +70,18 @@ def _get_quota_usage_and_limit(dir=None):
             dir = os.path.dirname(dir)
         except OSError as e:
             if e.errno == errno.ENOENT:
-                return (0, None)
+                return unknown
             raise
-    if not volume: return (0, None)
+    if not volume: return unknown
     try:
         result = sh.eval("vos", "examine", "-id", volume, "-cell", cell).splitlines()
     except OSError:
         try:
             result = sh.eval("/usr/sbin/vos", "examine", "-id", volume, "-cell", cell).splitlines()
         except OSError:
-            return (0, None)
+            return unknown
     except shell.CallError:
-        return (0, None)
+        return unknown
     try:
         usage = int(result[0].split()[3])
         limit = int(result[3].split()[1]) # XXX: FRAGILE
@@ -111,28 +89,6 @@ def _get_quota_usage_and_limit(dir=None):
         raise QuotaParseError("vos examine output was:\n\n" + "\n".join(result))
     return (usage, limit)
 
-# XXX: Possibly in the wrong module
-def get_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 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(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
-
 class QuotaParseError(wizard.Error):
     """Could not parse quota information."""
     def __init__(self, msg):
diff --git a/pull.sh b/pull.sh
index 53a85b7d81486dc67ec7c26a5fac176eabd7e224..0c1b57b1c883492ec43177c1357aba61b0458d7a 100755 (executable)
--- a/pull.sh
+++ b/pull.sh
@@ -3,3 +3,5 @@ aklog
 cd /mit/scripts/wizard
 git pull
 python setup.py egg_info
+cd plugins/scripts
+python setup.py egg_info
index b12c866101ec1408c64d180f605f5968ab80ca9b..29538e6af9a7c719696e49dc0870cf8e8ebd51e5 100755 (executable)
@@ -8,6 +8,6 @@ source ./mediawiki-install
 
 # crlf-ify the install
 FROB="RELEASE-NOTES"
-unix2dos "$TESTDIR/$FROB"
+unix2dos "$TESTDIR/$FROB" || todos "$TESTDIR/$FROB"
 
 wizard upgrade "$TESTDIR" --non-interactive
index eb600b6919e0a268c8f9e7f0fbe7b0df6c7feeb6..d8d875a4ce2bd60e3c6984e5bffeffe089da894c 100644 (file)
@@ -1 +1 @@
-WIZARD_TITLE="TestApp" wizard install "mediawiki-$VERSION-scripts" "$TESTDIR" --non-interactive -- --title="TestApp"
+wizard install "mediawiki-$VERSION-scripts" "$TESTDIR" --non-interactive -- --title="TestApp"
index 0f80601202c9cd202fe6f348987884b585a41480..4a1793e793ad36e655337e16275c4a0abd1f3011 100644 (file)
@@ -48,7 +48,7 @@ import tempfile
 import pkg_resources
 
 import wizard
-from wizard import resolve, scripts, shell, sql, util
+from wizard import resolve, shell, sql, util
 
 # SCRIPTS SPECIFIC
 _scripts_application_list = [
index bdcf25d1c9fa5357e83aedcbff6dd44051f47059..65b99e763f61501b3a3baabf149b781be6efdd3b 100644 (file)
@@ -50,16 +50,9 @@ class Application(app.Application):
     def checkConfig(self, deployment):
         return os.path.isfile("wp-config.php")
     def checkWeb(self, deployment):
-        # XXX: this sucks pretty hard
-        def doCheck():
-            return self.checkWebPage(deployment, "",
-                    outputs=["<html", "WordPress", "feed"],
-                    exclude=["Error establishing a database connection"])
-        if not doCheck():
-            deployment.enableOldStyleUrls()
-            return doCheck()
-        else:
-            return True
+        return self.checkWebPage(deployment, "",
+                outputs=["<html", "WordPress", "feed"],
+                exclude=["Error establishing a database connection"])
     def detectVersion(self, deployment):
         return self.detectVersionFromFile("wp-includes/version.php", php.re_var("wp_version"))
     def install(self, version, options):
index ebe1a4a14e83f22c8072bcdb3c040e92ecd19acd..d4f24e04a2776907093c95ffeb80c08e73d83673 100644 (file)
@@ -6,7 +6,7 @@ import sys
 import shutil
 import errno
 
-from wizard import deploy, report, scripts, shell, sset, command
+from wizard import deploy, report, shell, sset, command
 from wizard.command import upgrade
 
 def main(argv, baton):
index 18ae8d014c592fcc72ec7a25703487a6c9f9bd27..11fb04dd57ac207fab78eee0632c39afbf85b98d 100644 (file)
@@ -2,12 +2,12 @@ import logging
 import os.path
 import sys
 
-from wizard import command, scripts
+from wizard import command, user
 
 def main(argv, baton):
     options, args = parse_args(argv, baton)
     dir = os.path.abspath(args[0]) if args else os.getcwd()
-    r = scripts.get_quota_usage_and_limit(dir)
+    r = user.quota(dir)
     if r[0] is None or r[1] is None:
         sys.exit(1)
     print "%d %d" % r
index ecc0847847baa7d24b504db8fc2fa8ffa216a5c6..a3f2198c83831396946206dfdb2e1c5a7b8957b0 100644 (file)
@@ -9,9 +9,9 @@ import itertools
 import time
 import errno
 
-from wizard import app, command, deploy, merge, scripts, shell, util
+from wizard import app, command, deploy, merge, shell, user, util
 
-kib_buffer = 1024 * 30 # 30 MiB we will always leave available
+buffer = 1024 * 1024 * 30 # 30 MiB we will always leave available
 errno_blacklisted = 64
 
 def main(argv, baton):
@@ -136,8 +136,10 @@ class Upgrade(object):
         """Restore :attr:`prod` attribute, and check if the production copy has drifted."""
         self.prod = deploy.ProductionCopy(".")
         try:
-            shell.call("git", "status")
-            raise LocalChangesError()
+            r1 = shell.eval("git", "diff-files", "--name-only").strip()
+            r2 = shell.eval("git", "diff-index", "--name-only", "HEAD").strip()
+            if r1 or r2:
+                raise LocalChangesError()
         except shell.CallError:
             pass
         # Working copy is not anchored anywhere useful for git describe,
@@ -205,8 +207,8 @@ class Upgrade(object):
             sys.stderr.write("Traceback:\n  (n/a)\nAlreadyUpgraded\n")
             sys.exit(2)
     def preflightQuota(self):
-        kib_usage, kib_limit = scripts.get_quota_usage_and_limit()
-        if kib_limit is not None and (kib_limit - kib_usage) < kib_buffer:
+        usage, limit = user.quota()
+        if limit is not None and (limit - usage) < buffer:
             raise QuotaTooLow
 
     def merge(self):
@@ -269,7 +271,7 @@ class Upgrade(object):
         # yeah yeah no trailing newline whatever
         open(".git/WIZARD_UPGRADE_VERSION", "w").write(self.version)
         open(".git/WIZARD_PARENTS", "w").write("%s\n%s" % (self.user_commit, self.next_commit))
-        open(".git/WIZARD_SIZE", "w").write(str(scripts.get_disk_usage()))
+        open(".git/WIZARD_SIZE", "w").write(str(util.disk_usage()))
         if self.options.log_file:
             open(".git/WIZARD_LOG_FILE", "w").write(self.options.log_file)
     def mergePerform(self):
@@ -378,10 +380,10 @@ class Upgrade(object):
         # Ok, now we have to do a crazy complicated dance to see if we're
         # going to have enough quota to finish what we need
         pre_size = int(open(os.path.join(self.temp_wc_dir, ".git/WIZARD_SIZE"), "r").read())
-        post_size = scripts.get_disk_usage(self.temp_wc_dir)
+        post_size = util.disk_usage(self.temp_wc_dir)
         backup = self.prod.backup(self.options)
-        kib_usage, kib_limit = scripts.get_quota_usage_and_limit()
-        if kib_limit is not None and (kib_limit - kib_usage) - (post_size - pre_size) / 1024 < kib_buffer:
+        usage, limit = user.quota()
+        if limit is not None and (limit - usage) - (post_size - pre_size) < buffer:
             shutil.rmtree(os.path.join(".scripts/backups", shell.eval("wizard", "restore").splitlines()[0]))
             raise QuotaTooLow
         return backup
index 6d3ec6702f6ec3bef6520483eb1d1134cb23b52c..ea573c1fef5ef547c0368d063d7945e026aea43c 100644 (file)
@@ -14,9 +14,11 @@ import time
 import traceback
 import shutil
 import errno
+import pkg_resources
+import urlparse
 
 import wizard
-from wizard import app, git, old_log, scripts, shell, sql, util
+from wizard import app, git, old_log, shell, sql, util
 
 ## -- Global Functions --
 
@@ -73,6 +75,32 @@ def parse_install_lines(show, versions_store, yield_errors = False, user = None)
         # yield
         yield d
 
+def web(dir, url=None):
+    """
+    Attempts to determine the URL a directory would be web-accessible at.
+    If ``url`` is specified, automatically use it.  Returns a generator whic
+    produces a list of candidate urls.
+    """
+    if url:
+        yield url
+        return
+
+    for f in pkg_resources.iter_entry_points("deploy.web"):
+        for r in f(dir):
+            if isinstance(r, str):
+                r = urlparse.urlparse(r)
+            yield r
+
+    # try the environment
+    host = os.getenv("WIZARD_WEB_HOST")
+    path = os.getenv("WIZARD_WEB_PATH")
+    if host is not None and path is not None:
+        yield urlparse.ParseResult(
+                    "http",
+                    host,
+                    path.rstrip('/'),
+                    "", "", "")
+
 ## -- Model Objects --
 
 @decorator.decorator
@@ -106,6 +134,7 @@ class Deployment(object):
         self._old_log = None
         self._dsn = None
         self._url = None
+        self._urlGen = None
     def invalidateCache(self):
         """
         Invalidates all cached variables.  This currently applies to
@@ -304,16 +333,23 @@ class Deployment(object):
     @property
     def url(self):
         """The :class:`urlparse.ParseResult` for this deployment."""
-        if not self._url:
-            self._url = scripts.fill_url(self.location, self.application.url(self))
-        if not self._url:
-            raise UnknownWebPath
+        if not self._urlGen:
+            self._urlGen = web(self.location, self.application.url(self))
+            self.nextUrl()
         return self._url
+    def nextUrl(self):
+        """"""
+        try:
+            self._url = self._urlGen.next() # pylint: disable-msg=E1101
+            return self._url
+        except StopIteration:
+            raise UnknownWebPath
     def enableOldStyleUrls(self):
         """
         Switches to using http://user.scripts.mit.edu/~user/app URLs.
         No effect if they have an explicit .scripts/url override.
         """
+        # XXX: This is pretty scripts specific
         self._url = scripts.fill_url(self.location, self.application.url(self), old_style = True)
     @staticmethod
     def parse(line):
@@ -418,10 +454,18 @@ class ProductionCopy(Deployment):
             raise DatabaseVerificationError
     def verifyWeb(self):
         """
-        Checks if the autoinstall is viewable from the web.
+        Checks if the autoinstall is viewable from the web.  If you do not run
+        this, there is no guarantee that the url returned by this application
+        is the correct one.
         """
-        if not self.application.checkWeb(self):
-            raise WebVerificationError
+        while True:
+            if not self.application.checkWeb(self):
+                try:
+                    self.nextUrl()
+                except UnknownWebPath:
+                    raise WebVerificationError
+            else:
+                break
     def fetch(self, path, post=None):
         """
         Performs a HTTP request on the website.
index 291018cabf143b52b3b78603a6a60a95741964e0..64e3b09588d92bdcd4a68b8110c73b3fa91f0979 100644 (file)
@@ -26,7 +26,7 @@ import sqlalchemy
 import warnings
 
 import wizard
-from wizard import scripts, shell, sql, util
+from wizard import deploy, shell, sql, util
 
 def dsn_callback(options):
     if not isinstance(options.dsn, sqlalchemy.engine.url.URL):
@@ -114,15 +114,20 @@ class EnvironmentStrategy(Strategy):
 
 class ScriptsWebStrategy(Strategy):
     """Performs scripts specific guesses for web variables."""
+    # XXX: This actually isn't too scripts specific
     provides = frozenset(["web_host", "web_path"])
     def __init__(self, dir):
         self.dir = dir
     def prepare(self):
-        """Uses :func:`wizard.scripts.get_web_host_and_path`."""
+        """Uses :func:`deploy.web`."""
         if self.dir is None:
             raise StrategyFailed
-        self._url = scripts.fill_url(self.dir, None)
-        if not self._url:
+        urls = deploy.web(self.dir, None)
+        if not urls:
+            raise StrategyFailed
+        try:
+            self._url = urls.next()
+        except StopIteration:
             raise StrategyFailed
     def execute(self, options):
         """No-op."""
@@ -141,7 +146,7 @@ class ScriptsMysqlStrategy(Strategy):
         self.application = application
         self.dir = dir
     def prepare(self):
-        """Uses :func:`wizard.scripts.get_sql_credentials`"""
+        """Uses the SQL programs in the scripts locker"""
         if self.application.database != "mysql":
             raise StrategyFailed
         try:
diff --git a/wizard/tests/scripts_test.py b/wizard/tests/scripts_test.py
deleted file mode 100644 (file)
index 9cb120e..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-import os
-import unittest
-
-from wizard import tests
-from wizard.scripts import *
-
-class GetDiskUsageTest(unittest.TestCase):
-    def basicTest(self):
-        self.assertEqual(get_disk_usage(tests.getTestFile("scripts_test"), "ignore_me"), 7)
index ba57792718bc55c76ca6e1d479d7c0c1d70a7e9a..143913ddd3d23e9a5991875b7950349349358b1d 100644 (file)
@@ -83,3 +83,6 @@ def test_break_stale_lock():
     with LockDirectory(lockfile):
         with LockDirectory(lockfile, expiry = 0):
             pass
+
+def test_disk_usage():
+    assert disk_usage(tests.getTestFile("disk_usage_test"), "ignore_me") ==  7
diff --git a/wizard/user.py b/wizard/user.py
new file mode 100644 (file)
index 0000000..34a28f3
--- /dev/null
@@ -0,0 +1,23 @@
+"""
+Module for querying information about users.  This mostly asks plugins for
+the extra information, and falls back to using a default that should work
+on most systems (but by no means all systems.)
+"""
+
+import pkg_resources
+import os
+
+def quota(dir=None):
+    """
+    Returns a tuple (quota usage, quota limit).  Returns ``(0, None)`` if
+    the quota usage is unknown.  If ``dir`` is omitted, the current
+    working directory is assumed.  Value returned is in bytes.
+    """
+    if dir is None:
+        dir = os.getcwd()
+    unknown = (0, None)
+    for func in pkg_resources.iter_entry_points("wizard.user.quota"):
+        r = func(dir)
+        if r != unknown:
+            return r
+    return unknown
index 5f09ca3e84fb9246a142b15a6b4f806bf16a2bb4..c7d89690f1cc439881263bcfe88f6eff2e744205 100644 (file)
@@ -387,6 +387,28 @@ def mixed_newlines(filename):
     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))