QuotaFolder/0040775000076400007640000000000007446613250011504 5ustar ivoivoQuotaFolder/KNOWNBUGS0100664000076400007640000000412407446613232013002 0ustar ivoivoThis is a list of known issues / incompatibilities with QuotaFolder Bugs in Zope: - ZopeTutorial does some inproper exception catching, causing QuotaExceeded errors to be ignored (and actually the product to be imported twice). The relevant code is: try: folder.manage_importObject(tutorialExamplesFile) except: folder._p_jar=self.Destination()._p_jar folder.manage_importObject(tutorialExamplesFile) - A similair problem probably occurs when installing userfolders - if a user is past his quota and tries to create a userfolder, the error 'this folder already contains a userfolder', and the userfolder is created nonetheless This actually brings the quotafolder out of sync!!! (because due to the exception, the quota isn't updated, but the transaction isn't aborted either) -- RESOLVED 16Mar02 This bug has been worked around, though the error 'this object already has a userfolder' will appear if the quota is exceeded. Bugs in QuotaFolder: - Maximum object size is not always enforced, esp. when importing objects (or hierarchies of objects) - The following script used to give problems: doc = container.doc container.manage_delObjects(['doc']) doc.manage_edit('hello'*100, 'hacked') Hooked objects now check if they are still contained in their parent. What if doc is placed in a pseudo parent folder? - Pressing 'Sync' in the Quota tab brings you back to the Quota tab, however, the tab itself indicates 'Contents' is selected. -- RESOLVED 18Mar02 - QuotaExceeded exceptions aren't always precise - it displays how much quota you would need for the operation, but that may not be correct. - QuotaFolder seems to mess up undo capabilities (under what circumstances?) - QuotaFolder is not compatible with FLE. Creation goes fine (it seems), but newly created objects / modified objects are not added. -- RESOLVED 16Mar02 - It is still possible to store large amounts of data in properties. A solution would be to test if an object is an instance of PropertyManager, and account the size of the properties appropriately. QuotaFolder/DESIGN0100664000076400007640000000622407446613232012401 0ustar ivoivoPatch ObjectManager zdd: - het get_usage() ondersteunt Deze itereert over alle subobjects en roept, indien mogelijk, get_usage aan. Return: files, size - creatie van objecten verifieert met parent object Patch File en evt. andere objecten zdd. - deze get_usage() ondersteunen - manage_upload / whatever checken Quota awareness Implementeer een speciale QuotaFolder welke op basis van get_usage() gebruik bepaalt. Hou rekening met: - transparante migratie - sync optie om quota up2date te krijgen - Nesten van QuotaFolders - consistentie bij fouten/exceptions (i.e. QuotaExceeded) - hard/soft quota - mailnotificatie - security check in context van parent - wat als een folderstructuur gepaste wordt? Met een te-groot object? Is het zinnig quota etc bij te houden als er geen QuotaFolder parent is? Alleen als parent later QuotaFolder wordt... Kan dit? (migratie)? Waarom dan niet gewoon syncen? Nesten van QuotaFolders is uiteindelijk wel wenselijk, hier dient rekening mee gehouden te worden... - wat als folder gedelete wordt? - als data geupload/vergroot wordt? - check ftp, webdav - len(GET()) geeft rendered lengte? - len(.zexp)? Bij aanpassing van een object relatieve verschil quota_checken(), of recursief naar boven toe herberekenen? (geen optie bij grote site) ----------------------------------------------------------------------- Alle objecten en in het byzonder ObjectManagers worden quota aware gemaakt. Dit betekent dat: Objectmanagers - add/delete van objecten bijhouden (eigen count) en aan parent doorgeven - size van subobjecten bijhouden (recursief) - notificaties krijgen van size changes van subobjecten Andere Objecten (File, Image, DTMLDocument, PythonScript, ...) - eigen size bijhouden - uploads bijhouden en melden aan parent (=ObjectManager) - changes bijhouden en melden aan parent (beiden kunnen groei/krimp zijn) - stort het systeem in (afgezien van quotafolders) als er geen QuotaProduct meer is? - Ignore quota voor superuser - undo van (delete) transaction -> gevolgen? Toekomst: - soft/hard limit, expiry - mogelijkheid tot aanroepen script bij quotum exceeding (soft/hard) -> mail - Filteren/beperken meta types Specifiek testen: - exception bij overschrijding abort transaction? - support bij ftp, webdav (+ errorhandling) - photofolder - Btree folder - Transparentfolder? - Wat gebeurt er als je een quota aware folder importeert in een ander systeem? (niks! denk ik) - undo quota change door manager? - objecten deleten, tegen quotum aanzitten, deletion undo-en - versions ?! Lijken transparant te werken... (alhoewel je wel veel state in versions kan bewaren) - metapublisher/formulator scripts als: doc = container.doc container.manage_delObjects(['doc']) doc.manage_edit('hello'*100, 'hacked') Evt. met vervangende pseudo parent BUGS ---- - zcatalog aanmaken geeft 1 object, zcatalog deleten is -2 FIXED - tutorial geeft niet teveel objecten/size, deleten gaat fout FIXED - manage import/export voegt ook objecten toe zonder setOb FIXED - acquisition werkt mogelijk niet meteen/direct voor imported data FIXED Probleem: --------- Naast .zexp zijn ook pasted objecten objecten met veel subobjects (evt hierarchisch) QuotaFolder/QuotaFolder.py0100644000076400007640000002243707446613232014306 0ustar ivoivo## ## Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo@amaze.nl) ## ## All rights reserved. ## ## Redistribution and use in source and binary forms, with or without ## modification, are permitted provided that the following conditions ## are met: ## ## 1. Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## 2. Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in the ## documentation and/or other materials provided with the distribution. ## 3. The name of the author may not be used to endorse or promote products ## derived from this software without specific prior written permission. ## ## ## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## import App, Globals, OFS import string from Persistence import Persistent from Globals import DTMLFile from OFS.Folder import Folder from Acquisition import aq_base, aq_parent, Acquired, Implicit from AccessControl import getSecurityManager, SpecialUsers from zLOG import LOG, WARNING def manage_addQuotaFolder(self, id, title, quota_bytes=0, quota_objects=0, quota_maxsize=0, REQUEST=None): """ Add a new QuotaFolder """ o = QuotaFolder(id, title, quota_bytes, quota_maxsize, quota_objects, REQUEST) self._setObject(id, o) o = self._getOb(id) if REQUEST: return self.manage_main(self, REQUEST, update_menu=1) def _replaceFolder(parent, id, quota_bytes=0, quota_objects=0, quota_maxsize=0): # Replaces an OFS.Folder with a QuotaFolder. Borrowed from BTreeFolder ob = QuotaFolder(id, "", quota_bytes, quota_maxsize, quota_objects) f = parent._getOb(id) # Copy the contents of the folder to the new QuotaFolder ids = f.objectIds() for key, value in f.__dict__.items(): if key not in ids: # Not an ObjectManager item. ob.__dict__[key] = value for key in ids: subob = f._getOb(key) subob = getattr(subob, 'aq_base', subob) ob._setOb(key, subob) parent._setOb(id, ob) manage_addQuotaFolderForm=DTMLFile('manage_addQuotaFolderForm', globals()) QuotaExceededException='Quota Exceeded' class QuotaFolder(Folder, Persistent, Implicit): """ A QuotaFolder """ meta_type='QuotaFolder' manage_options=( Folder.manage_options+ ( {'label': 'Quota', 'action': 'manage_editQuotaForm'}, ) ) __ac_permissions__= \ Folder.__ac_permissions__ + \ ( ('View current quota', ('currentCount','currentSize')), ) manage_editQuotaForm=DTMLFile('manage_editQuotaForm', globals()) def __init__(self, id, title="", quota=0, maxFileSize=0, maxNumberOfFiles=0, manager_role=1, REQUEST=None): self.id=id self.title=title self._quota_bytes=int(quota) self._quota_objects=int(maxNumberOfFiles) self._quota_maxsize=int(maxFileSize) ## ## current accounting self._quota_filecount = 0 self._quota_size = 0 self._manager_role = 0 if manager_role: self._manager_role = 1 self._debug = 0 self._allow_nested = 0 def _check_quota(self, filechange, sizechange=0, objsize=0): """ Check if the change in usage violates the quota settings """ # # print "I'm a quota folder: ", filechange, sizechange, objsize # print "Current quota: ", self._quota_filecount, self._quota_size # # The emergency user can create *some* objects (i.e. userfolders). # make sure this never gives a QuotaExceeded exception emergency = 0 user=getSecurityManager().getUser() if (SpecialUsers.emergency_user and aq_base(user) is SpecialUsers.emergency_user): LOG('QuotaFolder', INFO, "Emergency user detected - not enforcing quota") emergency = 1 if not emergency and \ filechange > 0 and \ self._quota_filecount + filechange > self._quota_objects \ > 0: raise QuotaExceededException, \ "Too many files, this operation would require a quotum " + \ "of %d files, your current quotum is %d files" % \ (self._quota_filecount + filechange, self._quota_objects) if not emergency and \ sizechange > 0 and \ self._quota_size + sizechange > self._quota_bytes > 0: raise QuotaExceededException, \ "Too much space, this operation would require a " + \ "quotum of %d bytes, your current quotum is %d bytes" % \ (self._quota_size + sizechange, self._quota_bytes) if not emergency and objsize > self._quota_maxsize > 0: raise QuotaExceededException, \ "The object is too large. It requires %d bytes, " + \ "your maximum object size limit is %d bytes" % \ (objsize, self._quota_maxsize) self._quota_filecount = self._quota_filecount + filechange self._quota_size = self._quota_size + sizechange ## ## This should not happen... path = string.join(self.getPhysicalPath(), "/") if self._quota_filecount < 0: LOG('QuotaFolder', WARNING, "Negative quota filecount in %s" % path) if not self._debug: self._quota_filecount = 0 if self._quota_size < 0: LOG('QuotaFolder', WARNING, "Negative quota size in %s" % path) if not self._debug: self._quota_size = 0 # print "New quota: ", self._quota_filecount, self._quota_size def current_count(self): return self._quota_filecount def current_size(self): return self._quota_size def quota_bytes(self): return self._quota_bytes def quota_objects(self): return self._quota_objects def quota_maxsize(self): return self._quota_maxsize def authorized_to_edit(self): """ return true/false if user is authorized to change in current context """ security=getSecurityManager() security.addContext(self) user = security.getUser() if not self._manager_role: ## XXX use permission? return user.allowed(self, ['Manager']) ## use aq_base? return user.allowed(aq_parent(self), ['Manager']) def manager_role(self): return self._manager_role def manage_editQuota(self, quota=-1, maxFileSize=-1, maxNumberOfFiles=-1, REQUEST=None): """ Update the current Quota """ ## ## Perhaps always sync after edit? if REQUEST.has_key('Sync'): self._qa_sync() ## XXX report difference to parent? if REQUEST: ## A redirect selects the proper tab message = "Quota synchronized" REQUEST.RESPONSE.redirect( 'manage_editQuotaForm?manage_tabs_message=%s' % message) return self.manage_editQuotaForm(self, REQUEST, manage_tabs_message=message) return '' if not self.authorized_to_edit(): raise "Unauthorized", \ "You are not authorized to edit this quota folder" if quota != -1: self._quota_bytes=int(quota) if maxFileSize != -1: self._quota_maxsize=int(maxFileSize) if maxNumberOfFiles != -1: self._quota_objects=int(maxNumberOfFiles) self._manager_role = REQUEST.get("manager_role", 0) if REQUEST: message = "Changes saved" ## A redirect selects the proper tab REQUEST.RESPONSE.redirect( 'manage_editQuotaForm?manage_tabs_message=%s' % message) return self.manage_editQuotaForm(self, REQUEST, manage_tabs_message=message) return '' def manage_afterAdd(self, item, container): # print "afterAdd %s %s"%(str(item), str(container)) if not self._allow_nested: for parent in self.aq_chain[1:]: if hasattr(parent, "meta_type") and \ parent.meta_type == "QuotaFolder": raise QuotaExceededException, \ "Cannot add a QuotaFolder inside another QuotaFolder" Folder.manage_afterAdd(self, item, container) Globals.default__class_init__(QuotaFolder) QuotaFolder/TODO0100664000076400007640000000173707446613232012201 0ustar ivoivoFirst release: - check required attributes on objects (i.e. data on File/Image), report warning if missing - Provide a conversion script (external method) to convert standard folders to QuotaFolders DONE - Prevent 0-changes from propagating - properly determine size for TinyTablePlus objects - better QuotaExceeded exception strings DONE - ignore superuser, etc. DONE - add proper permissions DONE? - Do not raise exceptions on negative changes DONE - LICENSE Later release - support TinyTablePlus - soft/hard quota - Limit/restrict installable meta types - configurable size for objects without explicit size? (seems pointless) - configure max object size per meta type (+ limit # of instances) - optionally prevent quota updates from propagating beyond a folder (checkbox) -> this doesn't have to break nested quotafolders! (though it may give problems) QuotaFolder/__init__.py0100644000076400007640000003054207446613232013614 0ustar ivoivo## ## Copyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo@amaze.nl) ## ## All rights reserved. ## ## Redistribution and use in source and binary forms, with or without ## modification, are permitted provided that the following conditions ## are met: ## ## 1. Redistributions of source code must retain the above copyright ## notice, this list of conditions and the following disclaimer. ## 2. Redistributions in binary form must reproduce the above copyright ## notice, this list of conditions and the following disclaimer in the ## documentation and/or other materials provided with the distribution. ## 3. The name of the author may not be used to endorse or promote products ## derived from this software without specific prior written permission. ## ## ## THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR ## IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES ## OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ## IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, ## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT ## NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF ## THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## from zLOG import LOG, WARNING, BLATHER, INFO from OFS.ObjectManager import ObjectManager from Acquisition import aq_parent, aq_chain, aq_inner import QuotaFolder ## ## NOTE TO DEVELOPERS: ## ## Refreshing this product may not always work. If you do not get the expected ## behaviour after refreshing, try restarting your zopeserver! ## ## ## Idea: use *args / **args for wrapper methods and apply parameters to methods ## ## QA stands for Quota Aware, QU for Quota Unaware ## ## Invoke chain of _qf_check's, so all parents can update? (for delete of ## folders etc) def QA_setOb(self, id, object): ## ## Is it smarter to patch setObject instead? Is setOb ever invoked directly? ## ## We rely on the transaction being rolled back if an exception occurs. ## This means that QuotaExceeded exceptions shouldn't occur. Unfortunately, ## sometimes they are caught. if isinstance(object, ObjectManager): make_quota_aware(object) if hasattr(object, "_qa_sync"): object._qa_sync() _files, _size = QA_determine_usage(object) try: self._qf_check(_files, _size) except QuotaFolder.QuotaExceededException: ## ## This horrible fix is because manage_addUserFolder ignores all ## exceptions, and assumes there already is a UserFolder, while ## _setObject happily changes _objects as well while ignoring ## anything that may go wrong # print "O", self._objects o = [] ## ## Could this go wrong if there already is an object with id? ## (replacable?) (check using getattr?) if not hasattr(self, id): for i in self._objects: if i['id'] != id: o.append(i) self._objects = tuple(o) raise result = self._qu_setOb(id, object) return result def QA_delOb(self, id): # we can't use _getOb because it doesn't allow _attrs ## ## this works with BtreeFolders as well (well, with non-basicBtreeFolders) obj = getattr(self, id) try: _files,_size = QA_determine_usage(obj) except: LOG('QuotaProduct', BLATHER, "Can't get size for %s" % id) _files, _size = 1,0 self._qf_check(-_files, -_size) return self._qu_delOb(id) def getid(id): """ sometimes id is a string, sometimes a method """ if callable(id): return id() return id def findparent(object): """ find the direct parent """ for parent in aq_chain(object): if parent is object: continue if parent == object: continue if isinstance(parent, ObjectManager): return parent return None def QA_check_quota(self, filechange, sizechange=0, objsize=0): """ Notify a change in usage: filechange number of added/removed files sizechange change in size of usage objsize total size of object changes can be negative as well """ if hasattr(self, "_check_quota"): ## check hook for QuotaFolder self._check_quota(filechange, sizechange, objsize) else: ## do own administration make_quota_aware(self) self._quota_filecount = self._quota_filecount + filechange self._quota_size = self._quota_size + sizechange ## ## Acquisition is scary. The chain looks different when traversing ## through ftp/webdav. basically, 'self' may appear in the chain multiple ## times (with A NullResource in between), and may not always be equal ## through 'is' (so try == as well) ## ## Tip: use aq_base(parent) XXX for parent in aq_chain(self): if parent is self: continue if parent == self: continue if hasattr(parent, "_qf_check"): if hasattr(parent, getid(self.id)): parent._qf_check(filechange,sizechange,objsize) else: for name,ob in parent.objectItems(): if ob.id == getid(self.id): parent._qf_check(filechange,sizechange,objsize) return # !! break def QA_manage_importObject(self, file, REQUEST=None, set_owner=1): """ currently a useless hook. To be removed if not used """ result = apply(self._qu_manage_importObject, (file, REQUEST, set_owner)) return result def QA_sync(self): """ synchronize, i.e. recursively recalculate sizes """ self._quota_filecount = 0 # not 1, get_usage adds 1 self._quota_size = 0 for child in self.objectValues(): if hasattr(child, "_qa_sync"): child._qa_sync() _files, _size = QA_determine_usage(child) self._quota_filecount = self._quota_filecount + _files self._quota_size = self._quota_size + _size def make_quota_aware(object): if not hasattr(object, "_quota_filecount"): object._quota_filecount = 0 if not hasattr(object, "_quota_size"): object._quota_size = 0 def QA_get_usage(self): """ Return the size of an ObjectManager object. This is 1 + the number of subobjects. The size of an empty ObjectManager is considered to be 0 """ return self._quota_filecount+1, self._quota_size def QA_determine_usage(object): """ attempt to determine number of size and number of subobjects of object """ ## ## ZPT doesn't return the size of the unrendered content it seems, ## which leads to ## very inconsistent sizes. Let's hope this fixes it... if zpt_installed and isinstance(object, PageTemplate): # workaround for ZPT if hasattr(object, "_text"): return 1, len(object._text) return 1, 0 if hasattr(object, "_get_usage"): return object._get_usage() elif hasattr(object, "get_size"): ## ## Some products (such as PhotoFolders Photo) raise an exception ## when requested their size at init time try: _size = object.get_size() except: # can be any kind of exception LOG('QuotaProduct', BLATHER, 'Failed to get size for %s/%s' % \ (getid(object.id), object.meta_type)) _size = 0 return 1, _size elif hasattr(object, "getSize"): ## getSize is deprecated, but if it's all there is... return 1, object.getSize() else: LOG('QuotaProduct', BLATHER, "Can't get size for %s/%s" % \ (getid(object.id), object.meta_type)) return 1, 0 ## ## Patch the PythonScript object from Products.PythonScripts.PythonScript import PythonScript def QA_ps_write(self, text): """ invoked at creation, edit and upload - what else do you want? """ oldsize = self.get_size() result = self._qu_write(text) newsize = self.get_size() parent = findparent(self) if parent: if hasattr(parent, getid(self.id)): if hasattr(parent, "_qf_check"): parent._qf_check(0, newsize-oldsize, newsize) return result ## ## Patch DT_String, which effectively patches DTMLDocument and DTMLMethod from DocumentTemplate.DT_String import String def QA_dt_munge(self, source_string=None, mapping=None, **vars): oldsize = self.get_size() result = apply(self._qu_dt_munge, (source_string, mapping), vars) newsize = self.get_size() parent = findparent(self) if parent: if hasattr(parent, getid(self.id)): if hasattr(parent, "_qf_check"): parent._qf_check(0, newsize-oldsize, newsize) return result ## ## Patch File and Image from OFS.Image import File, Image def QA_file_update_data(self, data, content_type=None, size=None): ## ## This method may be invoked when self.data hasn't been initialized if not hasattr(self, "data"): oldsize = 0 else: oldsize = self.get_size() result = apply(self._qu_update_data, (data, content_type, size)) newsize = self.get_size() parent = findparent(self) if parent: if hasattr(parent, getid(self.id)): if hasattr(parent, "_qf_check"): parent._qf_check(0, newsize-oldsize, newsize) return result ## ## Patch BTreeFolder, if available btree_installed = 0 try: from Products.BTreeFolder.BTreeFolder import BTreeFolder LOG('QuotaProduct', INFO, "Patching BTreeFolder") btree_installed = 1 except ImportError: pass ## ## Patch ZopePageTemplate, if available zpt_installed = 0 try: from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate, PageTemplate LOG('QuotaProduct', INFO, "Patching ZPT") zpt_installed = 1 except ImportError: zpt_installed = 0 ## ## ZPT write hook def QA_zpt_write(self, text): oldsize = len(self._text) result = self._qu_write(text) newsize = len(self._text) parent = findparent(self) if parent: if hasattr(parent, getid(self.id)): if hasattr(parent, "_qf_check"): parent._qf_check(0, newsize-oldsize, newsize) return result ## ## Install all hooks ## ## Attempt to detect when we're refreshing and when we're restarting if not hasattr(ObjectManager, "_quota_aware"): ## ## save 'old' methods LOG('QuotaProduct', INFO, 'Detected server restart') ObjectManager._qu_setOb = ObjectManager._setOb ObjectManager._qu_delOb = ObjectManager._delOb ObjectManager._quota_aware = 1 ObjectManager._qu_manage_importObject = ObjectManager.manage_importObject PythonScript._qu_write = PythonScript.write String._qu_dt_munge = String.munge File._qu_update_data = File.update_data Image._qu_update_data = Image.update_data if btree_installed: BTreeFolder._qu_setOb = BTreeFolder._setOb BTreeFolder._qu_delOb = BTreeFolder._delOb if zpt_installed: ZopePageTemplate._qu_write = ZopePageTemplate.write else: LOG('QuotaProduct', INFO, 'Detected product refresh') ## ## Install the new methods. It won't hurt if this happens during a refresh ObjectManager._setOb = QA_setOb ObjectManager._delOb = QA_delOb ObjectManager.manage_importObject = QA_manage_importObject ObjectManager._qf_check = QA_check_quota ObjectManager._get_usage = QA_get_usage ObjectManager._qa_sync = QA_sync PythonScript.write = QA_ps_write String.munge = QA_dt_munge File.update_data = QA_file_update_data Image.update_data = QA_file_update_data if btree_installed: BTreeFolder._setOb = QA_setOb BTreeFolder._delOb = QA_delOb if zpt_installed: ZopePageTemplate.write = QA_zpt_write ## ## finally, install the QuotaFolder Product import QuotaFolder def initialize(context): try: context.registerClass( QuotaFolder.QuotaFolder, meta_type='QuotaFolder', permission='Add QuotaFolder', constructors=(QuotaFolder.manage_addQuotaFolderForm, QuotaFolder.manage_addQuotaFolder), ) except: import traceback traceback.print_exc() QuotaFolder/manage_addQuotaFolderForm.dtml0100664000076400007640000000361607446613232017423 0ustar ivoivo
Id
Title
Quota Size in Bytes (0 for unlimited)
Maximum Object Size in Bytes (0 for no limit)
Maximum number of Objects (0 for unlimited)
Require Manager role in parent context to edit
QuotaFolder/manage_editQuotaForm.dtml0100664000076400007640000000542607446613232016465 0ustar ivoivo
Current quota usage
bytes
Number of objects
Objects
Quota Size in Bytes (0 for no limit)
Maximum number of Objects (0 for unlimited)
Maximum Object Size in Bytes (0 for no limit)
Require Manager role in parent context to edit
CHECKED > (Warning: checking this box may revoke your rights to edit quota!) YES NO (You are not authorized to edit this quota folder)
QuotaFolder/refresh.txt0100664000076400007640000000023707446613232013702 0ustar ivoivo NOTE TO DEVELOPERS: Refreshing this product may not always work. If you do not get the expected behaviour after refreshing, try restarting your zopeserver! QuotaFolder/LICENSE0100664000076400007640000000264707446613232012517 0ustar ivoivoCopyright (c) 2002 Ivo van der Wijk, Amaze Internet Services (ivo@amaze.nl) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. QuotaFolder/README.txt0100644000076400007640000001735507446613232013210 0ustar ivoivoQuotaFolder - Quota support for Zope What is it? ----------- QuotaFolder is a folder-ish object that restricts the total number of objects, their total size and their individual maximum size. QuotaFolder takes subfolders (recursively) into account, so it should not be possible to escape the quota restrictions. The basic goal of QuotaFolder is not to put an absolute limit on ZODB usage - it is impossible to determine this. For example, each object may have several revisions in the ZODB, and some of the internal state of the object is stored in the ZODB that's not returned by get_size(). The goal is to limit the use of objects in general, to prevent people from offering large files or using up resources with enormous amounts of objects. How does it work? ----------------- QuotaFolder 'MonkeyPatches (tm)' some of the internal Zope components such as ObjectManager, File, Image, DTMLMethod, DTMLDocument and more. The patching constist of making the components 'quota aware'. This basically means the objects will track changes and report them to parent folders. If one if the parent folders is a QuotaFolder, and the QuotaFolder detects that the change will exceed the quota, a QuotaExceededException is raised, and the transaction is rolled back. At this moment, if an object supports the get_size() method, it's used. The value reported by this method will not take all usage (i.e. properties) into account, so this may be changed in the future. Supported products ------------------ The QuotaFolder product knows how to account File, Image, DTMLMethod, DTMLDocument and PythonScript objects. It also understands ObjectManager based products such as Folder and BTreeFolder (TransparentFolder is not thoroughly tested, but it seems to work well) Most products (i.e. FLE, Squishdot, PhotoFolder, ZWiki) exist of ObjectManagers with subobjects, and QuotaFolder knows how to handle these quite well. Unknown objects who's size (and changes in size) cannot be determined are accounted as 1 object with size 0. If this is not satisfactory (for example, it's not at this moment for TinyTablePlus), extra support can be built in for these objects (as has been done with, for example, ZPT). QuotaFolder also supports .zexp imports and copy/paste operations. QuotaFolder has been tested with Zope 2.4.3, 2.4.4b1 and 2.5.0. It has not been tested in ZEO setups. Installing ---------- **WARNING** This product patches classes in your Zope server. Please test the code on a test-server or shadow server first! This code has been known to work succesfully with Zope 2.4.x and Zope 2.5. Use this product at your own risk, and backup your Data.fs first! Install the QuotaFolder product by simply unpacking into your Products directory (either in your SOFTWARE_HOME or your INSTANCE_HOME) and restart your Zope server. The QuotaFolder product should appear in /Control_Panel/Products, and it should also be available in the products dropdown. When creating (or editting) a QuotaFolder, you will be presented with the following fields: id title Quota size in bytes - this is the maximum total size of all objects that's allowed to be created Maximum object size in bytes - The maximum allowed total size for single objects Maximum number of objects - this is the maximum number of objects that's allowed to be created Require manager role in parent context? - If this setting is enabled, a user must be manager in the folder context *above* the quotafolder itself to be able to edit the quota. If you want to limit your users in their usage, you don't want them to be able to edit their quota themselves, do you? After creation, you will get the same contents view as with a standard folder, but with an extra tab to the right where you can view and (optionally edit) the quota. When visiting the quota tab, you will see an extra button 'Sync'. Pressing this button will cause the QuotaFolder to recalculate all usage. Usually, the folder shouldn't be out of sync. If you manage to get a QuotaFolder out of sync, please contact me. Migrating --------- There are two ways to migrate a standard folder to a QuotaFolder: - Create a new QuotaFolder, copy all objects from the old folder, paste them into the QuotaFolder and rename the folders. This will probably not work if you have versions (cut/paste may work with versions) - Use the builtin _replaceFolder method. I.e. create the following external method: from Products.QuotaFolder.QuotaFolder import _replaceFolder def replace(self, name, quota_bytes, quota_objects, quota_maxsize, REQUEST): _replaceFolder(self, name, quota_bytes, quota_objects, quota_maxsize) return "%s converted" % name Create an appropriate external method object in your zope server and invoke it with an appropriately formatted url or create a dtml form. Hacking contest! ---------------- QuotaFolder has been thoroughly tested, and seems very stable and compatible. However, every now and then, new situations seem to appear where it's possible to use more objects or space than the QuotaFolder should enforce. This may be through unsupported objects (though these should generaly not make it possible to create more free space for other objects), or using trickery with PythonScripts, etc. If anyone finds issues like this, please contact me (info below), so we can make this product even more stable and robust :) Please check the file KNOWNBUGS before reporting issues. Release info ------------ 0.1 Initial version, basic support for quota Future plans ------------ - Support soft and hardlimit quota, with configurable timeleft - Restrict installable metatypes - Limit number of certain metatypes Contact/License --------------- QuotaFolder is partially based on ideas by Andrew Kenneth, though most of his old QuotaFolder has disappeared. QuotaFolder is written by Ivo van der Wijk as part of Amaze Internet Service's FreeZope.org Free Zope hosting environment. I can be contacted through ivo@amaze.nl or on IRC as VladDrac / VladDrak @ OPN Recent versions of QuotaFolder and other Zope products can be found at: http://www.zope.org/Members/ivo http://vanderijk.info/ QuotaFolder is (c) 2002 Ivo van der Wijk / Amaze Internet Services All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.