# -*- test-case-name: mv3d.test.test_asset -*- # Copyright (C) 2006-2012 Mortal Coil Games # See LICENSE for details. """ A class for an Asset that can be downloaded by a URL. The URL is in standard format, and can be one of the following types: http:// https:// (somewhat untested, use at your own risk) """ import md5 import os import sys import logging from twisted.internet import defer from twisted.internet.defer import inlineCallbacks, returnValue from twisted.web.client import downloadPage from twisted.web import error from mv3d.util.iservice import IPlayerClient import mv3d from mv3d.util.classgen import ClassGenerator from mv3d.resource.asset import ( ImageAsset, MaterialAsset, MeshAsset, PhysicsAsset, ShaderAsset) from mv3d.resource.asset import CodeAsset, SoundAsset, FileAsset from mv3d.net.ha import withSlaveUpdate from mv3d.util.persist import Inclusive, Text, SharedID, List class URLAssetError(Exception): """ Raised when there is a problem with a URL Asset """ class URLAsset: """ An asset that can be downloaded via a URL Files are saved in basedir/GroupXX/localfile (XX = Asset Group number) """ _baseType = Inclusive() localfile = List(Text()) url = Text(default="", autoSave=True, partialSave=True) basedir = Text(default="Extern", autoSave=True, partialSave=True) allowedState = ["basedir", "url", "localfile"] useMV3DPath = True def getURL(self): """ Get the URL this asset can be downloaded from """ return self.url @withSlaveUpdate def setURL(self, u): """ Set the URL this Asset can be downloaded from """ self.url = u def getLocalFile(self): """ Gets the local file name for this asset """ return os.path.join(*self.localfile) @withSlaveUpdate def setLocalFile(self, f): """ Sets the local filename for this asset """ self.localfile = f self.queueSave(selectAttributes=["localfile"]) def getBaseDir(self): """ return the current base dir """ return self.basedir @withSlaveUpdate def setBaseDir(self, b): """ Set the base directory for this asset """ self.basedir = b observe_setURL = setURL observe_setLocalFile = setLocalFile observe_setBaseDir = setBaseDir @classmethod def getBasePath(cls, conductor, groupID): """ Returns the base path """ return os.path.join(conductor.dataRoot, "Extern", "Group%d" % groupID) def getFullPath(self): """ Returns the full path to where our file is located """ if self.useMV3DPath: if self.parent is not None: return os.path.join(self.parent.parent.dataRoot, self.basedir, "Group%d" % self.getID()[0], *self.localfile[:-1]) else: return os.path.join(os.path.dirname(mv3d.__file__), "..", self.basedir, "Group%d" % self.getID()[0], *self.localfile[:-1]) else: return os.path.join(self.basedir, "Group%d" % self.getID()[0], *self.localfile[:-1]) def checkPath(self): """ Checks to make sure that the path to our file exists. if not, create it. """ p = self.getFullPath() if os.path.exists(p): return True os.makedirs(p) return True def getFullFile(self): """ Returns full path + local file name """ if self.localfile is None: raise ValueError("Local file is None and this is invalid.") if self.useMV3DPath: if self.parent is not None: return os.path.join(self.parent.parent.dataRoot, self.basedir, "Group%d" % self.getID()[0], *self.localfile) else: return os.path.abspath(os.path.join(os.path.dirname( mv3d.__file__), "..", self.basedir, "Group%d" % self.getID()[0], *self.localfile)) else: return os.path.abspath(os.path.join( self.basedir, "Group%d" % self.getID()[0], *self.localfile)) def getFile(self): """ Returns just the file name """ return self.localfile[-1] @inlineCallbacks def urlAcquireAsset(self, downloadList=None, **kw): """ Download this asset using urllib """ if self.localfile is None or "".join(self.localfile) == "": raise ValueError("Can't download from %s with no local file" % self.url) if not self.url: raise ValueError("Can't download from %s.(%r)" % (self.url, self.getFullFile())) yield self.acquireDependencies(downloadList=downloadList, **kw) if self.urlHaveAsset(): returnValue(True) if self.parent is not None: self.parent.parent.log("Downloading %s from %s." % ( self.getFile(), self.url), logLevel=logging.INFO, system=self.parent.name) if not self.checkPath(): raise URLAssetError("Check path failed") try: yield downloadPage(str(self.url), self.getFullFile(), agent="MV3D Asset Downloader") except error.Error, e: raise URLAssetError(str(e)) if self.parent is not None: self.parent.parent.log("Downloaded %s." % ( self.getFile()), logLevel=logging.INFO, system=self.parent.name) returnValue(True) def urlHaveAsset(self): """ This version of haveAsset checks if the file exists and then generates the md5 sum of it to check the CRC """ # print "Haveasset start" if self.url is None: # don't even bother downloading return True if not os.path.exists(self.getFullFile()): return False if self.checksum is None: # no checksum, so we have the asset I guess. return True #print "continuting", self.checksum f = open(self.getFullFile(), "rb") s = f.read() f.close() m = md5.new() m.update(s) if m.hexdigest() != self.checksum: return False #print "Have asset full" return True class URLImageAsset(ImageAsset, URLAsset): """ An image asset that can be retrieved by a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + ImageAsset.allowedState classModifiers = dict(web=[ ClassGenerator("mv3d.server.editor.URLAssetEditor"), ClassGenerator("mv3d.server.editor.DeleteAsset"), ]) readied = False def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): ImageAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Check if we have the asset locally. Also ready it with the renderer """ if not self.urlHaveAsset(): return False self.readyAsset() return True def readyAsset(self): """ Ready this asset """ player = self.parent.parent.getLocalService(IPlayerClient, None) if player is None: return player.getRenderer().addResourceLocation(os.path.dirname(self.getFullFile())) self.readied = True def acquireAsset(self, downloadList=None, **kw): """ Download the asset from the url """ d = self.urlAcquireAsset(downloadList=downloadList, **kw) def register(r): self.readyAsset() return r if isinstance(d, defer.Deferred): d.addCallback(register) return d return register(d) class URLMaterialAsset(MaterialAsset, URLAsset): """ A material asset that can be retrieved by a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + MaterialAsset.allowedState def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): MaterialAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if the asset has been downloaded """ return self.urlHaveAsset() def acquireAsset(self, downloadList=None): """ Retrieve the asset """ return self.urlAcquireAsset(downloadList) class URLShaderAsset(ShaderAsset, URLAsset): """ A shader asset that can be retrieved by a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + ShaderAsset.allowedState def __init__(self, url=None, localfile=None, server=None, basedir=u"Extern"): ShaderAsset.__init__(self) self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if the asset has been downloaded """ return self.urlHaveAsset() def acquireAsset(self, downloadList=None): """ Retrieve the asset """ return self.urlAcquireAsset(downloadList) class URLMeshAsset(MeshAsset, URLAsset): """ A mesh asset that can be retrieved by a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + MeshAsset.allowedState def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): MeshAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if we have the asset locally """ return URLAsset.urlHaveAsset(self) def acquireAsset(self, downloadList=None): """ Retrieve the asset """ return URLAsset.urlAcquireAsset(self, downloadList) class URLPhysicsAsset(PhysicsAsset, URLAsset): """ A physics asset (collision geometry, etc) that can be retrieved via a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + PhysicsAsset.allowedState def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): PhysicsAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if we have the asset locally """ return URLAsset.urlHaveAsset(self) def acquireAsset(self, downloadList=None): """ Retrieves the asset from the url """ return URLAsset.urlAcquireAsset(self, downloadList) class URLCodeAsset(CodeAsset, URLAsset): """ A code module that can be retrieved via a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + CodeAsset.allowedState def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): CodeAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if we have the asset locally """ return URLAsset.urlHaveAsset(self) def acquireAsset(self, downloadList=None): """ Retrieve the asset from the url """ return URLAsset.urlAcquireAsset(self, downloadList) class URLSoundAsset(SoundAsset, URLAsset): """ A sound that can be retrieved from a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + SoundAsset.allowedState def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): SoundAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if we have the asset locally """ return URLAsset.urlHaveAsset(self) def acquireAsset(self, downloadList=None): """ Retrieves the asset from the url """ return URLAsset.urlAcquireAsset(self, downloadList) class URLFileAsset(FileAsset, URLAsset): """ A generic file asset that can be retrieved from a url """ _schemaVersion = 2 _baseType = SharedID() allowedState = URLAsset.allowedState + FileAsset.allowedState classModifiers = dict(web=[ ClassGenerator("mv3d.server.editor.URLAssetEditor"), ClassGenerator("mv3d.server.editor.DeleteAsset"), ]) def __init__(self, datastore=None, url=None, localfile=None, server=None, basedir=u"Extern"): FileAsset.__init__(self) self.datastore = datastore self.url = url self.localfile = localfile self.basedir = basedir self.parent = server def haveAsset(self): """ Returns true if we have the asset locally """ return URLAsset.urlHaveAsset(self) def acquireAsset(self, downloadList=None): """ Retrieve the asset from the url """ return URLAsset.urlAcquireAsset(self, downloadList) def newURLFileAsset(conductor, assetgroup, url, localfile=u"", name=u"", depends=None): """ Factory to create a file asset """ d = assetgroup.newAsset(conductor, ClassGenerator( classname=u"URLFileAsset", modulename=u"mv3d.resource.url")) def finishUp(tm, url, localfile, name): """ Set the properties of the newly created asset. """ tm.url = url if localfile != "": tm.localfile = localfile if name != "": tm.name = name for d in depends or []: if isinstance(d, tuple): tm.addDependency(d) else: tm.addDependency(d.getID()) tm.grantPermission("read", "all") tm.grantPermission("reference", "all") tm.queueSave(selectAttributes=["url", "localfile", "name"]) return tm d.addCallback(finishUp, url, localfile, name) return d def newURLImageAsset(conductor, assetgroup, url, localfile=u"", name=u""): """ Factory to create a new url image asset """ d = assetgroup.newAsset(conductor, ClassGenerator( classname=u"URLImageAsset", modulename=u"mv3d.resource.url")) def finishUp(tm, url, localfile, name): """ Set the properties of the newly created asset """ tm.url = url if localfile != "": tm.localfile = localfile if name != "": tm.name = name tm.grantPermission("read", "all") tm.grantPermission("reference", "all") tm.queueSave(selectAttributes=["url", "localfile", "name"]) return tm d.addCallback(finishUp, url, localfile, name) return d def newURLClassGeneratorAsset(conductor, assetgroup, url, classn, localfile="", name="", depends=None): """ A factory to create a new url class generator asset """ if depends is None: depends = [] tm = assetgroup.newAsset(conductor, ClassGenerator( classname="URLClassGeneratorAsset", modulename="mv3d.resource.url")) def finishUp(tm, url, classn, localfile, name, depends): """ Set the properties of the newly created asset """ tm.url = url tm.classname = classn if localfile != "": tm.localfile = localfile if name != "": tm.name = name for d in depends: if isinstance(d, tuple): tm.addDependency(d) else: tm.addDependency(d.getID()) tm.grantPermission("read", "all") tm.grantPermission("reference", "all") tm.queueSave(selectAttributes=["url", "localfile", "name"]) return tm if isinstance(tm, defer.Deferred): tm.addCallback(finishUp, url, classn, localfile, name, depends) return tm return finishUp(tm, url, classn, localfile, name, depends) class URLClassGeneratorAsset(URLCodeAsset): """ A class generator asset where the code can be downloaded via url """ _schemaVersion = 2 classname = Text(default="", autoSave=True, partialSave=True, transmit=True) classModifiers = dict(web=[ ClassGenerator("mv3d.server.editor.URLClassGeneratorAssetEditor"), ClassGenerator("mv3d.server.editor.DeleteAsset"), ]) def __init__(self, datastore=None, server=None, url=None, localfile=None, basedir=u"Extern", classname=None): URLCodeAsset.__init__(self, datastore, url, localfile, server, basedir) self.classname = classname self.server = server def getModule(self): """ Returns the module name """ f = self.getLocalFile()[:-3] f = "Group%d" % self.getID()[0] + "." + f.replace(os.path.sep, ".") return f def getClass(self): """ Returns the class name """ return self.classname @withSlaveUpdate def setClass(self, f): """ Sets the class name """ self.classname = f def checkBaseDir(self): """ Makes sure that the base dir exists. """ if not self.getBaseDir() in sys.path: sys.path.append(self.getBaseDir()) mdfile = (self.getBaseDir() + os.sep + "Group%d" % self.getID()[0] + os.sep + "__init__.py") if not os.path.exists(mdfile): # just create it empty f = open(mdfile, "w") f.write("\"\"\" Init file for Resource Group %d \"\"\"" % self.getID()[0]) f.close() def getCG(self): """ Returns a class generator to construct this asset """ self.checkBaseDir() return ClassGenerator(modulename=self.getModule(), classname=self.classname, assetid=self.getID(), assetService=self.parent) def acquireAsset_gotAsset(self, r): """ Callback function for when the asset is retrieved that makes sure all the directories that are expected to be python packages have __init__ files. """ URLCodeAsset.acquireAsset_gotAsset(self, r) self.checkBaseDir() # now check to see if we have already loaded the module.... if sys.modules.has_key(self.getModule()): # ok, we need to reload to upgrade to the # newest version. self.server.RefreshModule(self.getModule()) return 1