home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / olddatabaseupgrade.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  19.3 KB  |  561 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. """Module used to upgrade from databases before we had our current scheme.
  19.  
  20. Strategy:
  21. * Unpickle old databases using a subclass of pickle.Unpickle that loads
  22.     fake class objects for all our DDBObjects.  The fake classes are just
  23.     empty shells with the upgrade code that existed when we added the schema
  24.     module.
  25.  
  26. * Save those objects to disk, using the initial schema of the new system.
  27.  
  28. """
  29.  
  30. from new import classobj
  31. from copy import copy
  32. from datetime import datetime
  33. import pickle
  34. import shutil
  35. import threading
  36. import types
  37. import time
  38.  
  39. from schema import ObjectSchema, SchemaInt, SchemaFloat, SchemaSimpleItem
  40. from schema import SchemaObject, SchemaBool, SchemaDateTime, SchemaTimeDelta
  41. from schema import SchemaList, SchemaDict
  42. from fasttypes import LinkedList
  43. from types import NoneType
  44. import storedatabase
  45.  
  46. ######################### STAGE 1 helpers  #############################
  47. # Below is a snapshot of what the database looked like at 0.8.2.  DDBObject
  48. # classes and other classes that get saved in the database are present only as
  49. # skeletons, all we want from them is their __setstate__ method.  
  50. #
  51. # The __setstate_ methods are almost exactly like they were in 0.8.2.  I
  52. # removed some things that don't apply to us simple restoring, then saving the
  53. # database (starting a Thread, sending messages to the downloader daemon,
  54. # etc.).  I added some things to make things compatible with our schema,
  55. # mostly this means setting attributes to None, where before we used the fact
  56. # that access the attribute would throw an AttributeError (ugh!).
  57. #
  58. # We prepend "Old" to the DDBObject so they're easy to recognize if
  59. # somehow they slip through to a real database
  60. #
  61. # ObjectSchema
  62. # classes are exactly as they appeared in version 6 of the schema.
  63. #
  64. # Why version 6?
  65. # Previous versions were in RC's.  They dropped some of the data that we
  66. # need to import from old databases By making olddatabaseupgrade start on
  67. # version 6 we avoid that bug, while still giving the people using version 1
  68. # and 2 an upgrade path that does something.
  69.  
  70. def defaultFeedIconURL():
  71.     import resources
  72.     return resources.url("images/feedicon.png")
  73.  
  74. #Dummy class for removing bogus FileItem instances
  75. class DropItLikeItsHot(object):
  76.     __DropMeLikeItsHot = True
  77.     def __slurp(self, *args, **kwargs):
  78.         pass
  79.     def __getattr__(self, attr):
  80.         if attr == '__DropMeLikeItsHot':
  81.             return self.__DropMeLikeItsHot
  82.         else:
  83.             print "DTV: WARNING! Attempt to call '%s' on DropItLikeItsHot instance" % attr
  84.             import traceback
  85.             traceback.print_stack()
  86.             return self.__slurp
  87.     __setstate__ = __slurp
  88.     def __repr__(self):
  89.         return "DropMeLikeItsHot"
  90.     def __str__(self):
  91.         return "DropMeLikeItsHot"
  92.  
  93. class OldDDBObject(object):
  94.     pass
  95.  
  96.  
  97. class OldItem(OldDDBObject):
  98.     # allOldItems is a hack to get around the fact that old databases can have
  99.     # items that aren't at the top level.  In fact, they can be in fairly
  100.     # crazy places.  See bug #2515.  So we need to keep track of the items
  101.     # when we unpickle the objects.
  102.     allOldItems = set()
  103.  
  104.     def __setstate__(self, state):
  105.         (version, data) = state
  106.         if version == 0:
  107.             data['pendingManualDL'] = False
  108.             if not data.has_key('linkNumber'):
  109.                 data['linkNumber'] = 0
  110.             version += 1
  111.         if version == 1:
  112.             data['keep'] = False
  113.             data['pendingReason'] = ""
  114.             version += 1
  115.         if version == 2:
  116.             data['creationTime'] = datetime.now()
  117.             version += 1
  118.         assert(version == 3)
  119.         data['startingDownload'] = False
  120.         self.__dict__ = data
  121.  
  122.         # Older versions of the database allowed Feed Implementations
  123.         # to act as feeds. If that's the case, change feed attribute
  124.         # to contain the actual feed.
  125.         # NOTE: This assumes that the feed object is decoded
  126.         # before its items. That appears to be generally true
  127.         if not issubclass(self.feed.__class__, OldDDBObject):
  128.             try:
  129.                 self.feed = self.feed.ufeed
  130.             except:
  131.                 self.__class__ = DropItLikeItsHot
  132.             if self.__class__ is OldFileItem:
  133.                 self.__class__ = DropItLikeItsHot
  134.  
  135.         self.iconCache = None
  136.         if not 'downloadedTime' in data:
  137.             self.downloadedTime = None
  138.         OldItem.allOldItems.add(self)
  139.  
  140. class OldFileItem(OldItem):
  141.     pass
  142.  
  143. class OldFeed(OldDDBObject):
  144.     def __setstate__(self,state):
  145.         (version, data) = state
  146.         if version == 0:
  147.             version += 1
  148.         if version == 1:
  149.             data['thumbURL'] = defaultFeedIconURL()
  150.             version += 1
  151.         if version == 2:
  152.             data['lastViewed'] = datetime.min
  153.             data['unwatched'] = 0
  154.             data['available'] = 0
  155.             version += 1
  156.         assert(version == 3)
  157.         data['updating'] = False
  158.         if not data.has_key('initiallyAutoDownloadable'):
  159.             data['initiallyAutoDownloadable'] = True
  160.         self.__dict__ = data
  161.         # This object is useless without a FeedImpl associated with it
  162.         if not data.has_key('actualFeed'):
  163.             self.__class__ = DropItLikeItsHot
  164.  
  165.         self.iconCache = None
  166.  
  167. class OldFolder(OldDDBObject):
  168.     pass
  169.  
  170. class OldHTTPAuthPassword(OldDDBObject):
  171.     pass
  172.  
  173. class OldFeedImpl:
  174.     def __setstate__(self, data):
  175.         self.__dict__ = data
  176.         if 'expireTime' not in data:
  177.             self.expireTime = None
  178.  
  179.         # Some feeds had invalid updating freq.  Catch that error here, so we
  180.         # don't lose the dabatase when we restore it.
  181.         try:
  182.             self.updateFreq = int(self.updateFreq)
  183.         except ValueError:
  184.             self.updateFreq = -1
  185.  
  186. class OldScraperFeedImpl(OldFeedImpl):
  187.     def __setstate__(self,state):
  188.         (version, data) = state
  189.         assert(version == 0)
  190.         data['updating'] = False
  191.         data['tempHistory'] = {}
  192.         OldFeedImpl.__setstate__(self, data)
  193.  
  194. class OldRSSFeedImpl(OldFeedImpl):
  195.     def __setstate__(self,state):
  196.         (version, data) = state
  197.         assert(version == 0)
  198.         data['updating'] = False
  199.         OldFeedImpl.__setstate__(self, data)
  200.  
  201. class OldSearchFeedImpl(OldRSSFeedImpl):
  202.     pass
  203.  
  204. class OldSearchDownloadsFeedImpl(OldFeedImpl):
  205.     pass
  206.  
  207. class OldDirectoryFeedImpl(OldFeedImpl):
  208.     def __setstate__(self,state):
  209.         (version, data) = state
  210.         assert(version == 0)
  211.         data['updating'] = False
  212.         if not data.has_key('initialUpdate'):
  213.             data['initialUpdate'] = False
  214.         OldFeedImpl.__setstate__(self, data)
  215.  
  216. class OldRemoteDownloader(OldDDBObject):
  217.     def __setstate__(self,state):
  218.         (version, data) = state
  219.         self.__dict__ = copy(data)
  220.         self.status = {}
  221.         for key in ('startTime', 'endTime', 'filename', 'state',
  222.                 'currentSize', 'totalSize', 'reasonFailed'):
  223.             self.status[key] = self.__dict__[key]
  224.             del self.__dict__[key]
  225.         # force the download daemon to create a new downloader object.
  226.         self.dlid = 'noid'
  227.  
  228. class OldChannelGuide(OldDDBObject):
  229.     def __setstate__(self,state):
  230.         (version, data) = state
  231.  
  232.         if version == 0:
  233.             self.sawIntro = data['viewed']
  234.             self.cachedGuideBody = None
  235.             self.loadedThisSession = False
  236.             self.cond = threading.Condition()
  237.         else:
  238.             assert(version == 1)
  239.             self.__dict__ = data
  240.             self.cond = threading.Condition()
  241.             self.loadedThisSession = False
  242.         if not data.has_key('id'):
  243.             self.__class__ = DropItLikeItsHot
  244.  
  245.         # No need to load a fresh channel guide here.
  246.  
  247. class OldMetainfo(OldDDBObject):
  248.     pass
  249.  
  250. fakeClasses = {
  251.     'item.Item': OldItem,
  252.     'item.FileItem': OldFileItem,
  253.     'feed.Feed': OldFeed,
  254.     'feed.FeedImpl': OldFeedImpl,
  255.     'feed.RSSFeedImpl': OldRSSFeedImpl,
  256.     'feed.ScraperFeedImpl': OldScraperFeedImpl,
  257.     'feed.SearchFeedImpl': OldSearchFeedImpl,
  258.     'feed.DirectoryFeedImpl': OldDirectoryFeedImpl,
  259.     'feed.SearchDownloadsFeedImpl': OldSearchDownloadsFeedImpl,
  260.     'downloader.HTTPAuthPassword': OldHTTPAuthPassword,
  261.     'downloader.RemoteDownloader': OldRemoteDownloader,
  262.     'guide.ChannelGuide': OldChannelGuide,
  263.  
  264.     # Drop these classes like they're hot!
  265.     #
  266.     # YahooSearchFeedImpl is a leftover class that we don't use anymore.
  267.     #
  268.     # The HTTPDownloader and BTDownloader classes were removed in 0.8.2.  The
  269.     # cleanest way to handle them is to just drop them.  If the user still has
  270.     # these in their database, too bad.  BTDownloaders may contain BTDisplay
  271.     # and BitTorrent.ConvertedMetainfo.ConvertedMetainfo objects, drop those
  272.     # too.
  273.     #
  274.     # We use BitTornado now, so drop the metainfo... We should recreate it
  275.     # after the upgrade.
  276.     #
  277.     # DownloaderFactory and StaticTab shouldn't be pickled, but I've seen
  278.     # databases where it is.
  279.     # 
  280.     # We used to have classes called RSSFeed, ScraperFeed, etc.  Now we have
  281.     # the Feed class which contains a FeedImpl subclass.  Since this only
  282.     # happens on really old databases, we should just drop the old ones.
  283.     'BitTorrent.ConvertedMetainfo.ConvertedMetainfo': DropItLikeItsHot,
  284.     'downloader.DownloaderFactory': DropItLikeItsHot,
  285.     'app.StaticTab': DropItLikeItsHot,
  286.     'feed.YahooSearchFeedImpl': DropItLikeItsHot,
  287.     'downloader.BTDownloader': DropItLikeItsHot,
  288.     'downloader.BTDisplay': DropItLikeItsHot,
  289.     'downloader.HTTPDownloader': DropItLikeItsHot,
  290.     'scheduler.ScheduleEvent': DropItLikeItsHot,
  291.     'feed.UniversalFeed' : DropItLikeItsHot,
  292.     'feed.RSSFeed': DropItLikeItsHot,
  293.     'feed.ScraperFeed': DropItLikeItsHot,
  294.     'feed.SearchFeed': DropItLikeItsHot,
  295.     'feed.DirectoryFeed': DropItLikeItsHot,
  296.     'feed.SearchDownloadsFeed': DropItLikeItsHot,
  297. }
  298.  
  299.  
  300. class FakeClassUnpickler(pickle.Unpickler):
  301.     unpickleNormallyWhitelist = [
  302.         'datetime.datetime', 
  303.         'datetime.timedelta', 
  304.         'time.struct_time',
  305.         'feedparser.FeedParserDict',
  306.         '__builtin__.unicode',
  307.     ]
  308.  
  309.     def find_class(self, module, name):
  310.         fullyQualifiedName = "%s.%s" % (module, name)
  311.         if fullyQualifiedName in fakeClasses:
  312.             return fakeClasses[fullyQualifiedName]
  313.         elif fullyQualifiedName in self.unpickleNormallyWhitelist:
  314.             return pickle.Unpickler.find_class(self, module, name)
  315.         else:
  316.             raise ValueError("Unrecognized class: %s" % fullyQualifiedName)
  317.  
  318. class IconCache:
  319.     # We need to define this class for the ItemSchema.  In practice we will
  320.     # always use None instead of one of these objects.
  321.     pass
  322.  
  323.  
  324. ######################### STAGE 2 helpers #############################
  325.  
  326. class DDBObjectSchema(ObjectSchema):
  327.     klass = OldDDBObject
  328.     classString = 'ddb-object'
  329.     fields = [
  330.         ('id', SchemaInt())
  331.     ]
  332.  
  333. # Unlike the SchemaString in schema.py, this allows binary strings or
  334. # unicode strings
  335. class SchemaString(SchemaSimpleItem):
  336.     def validate(self, data):
  337.         super(SchemaSimpleItem, self).validate(data)
  338.         self.validateTypes(data, (unicode, str))
  339.  
  340. # Unlike the simple container in schema.py, this allows binary strings
  341. class SchemaSimpleContainer(SchemaSimpleItem):
  342.     """Allows nested dicts, lists and tuples, however the only thing they can
  343.     store are simple objects.  This currently includes bools, ints, longs,
  344.     floats, strings, unicode, None, datetime and struct_time objects.
  345.     """
  346.  
  347.     def validate(self, data):
  348.         super(SchemaSimpleContainer, self).validate(data)
  349.         self.validateTypes(data, (dict, list, tuple))
  350.         self.memory = set()
  351.         toValidate = LinkedList()
  352.         while data:
  353.             if id(data) in self.memory:
  354.                 return
  355.             else:
  356.                 self.memory.add(id(data))
  357.     
  358.             if isinstance(data, list) or isinstance(data, tuple):
  359.                 for item in data:
  360.                     toValidate.append(item)
  361.             elif isinstance(data, dict):
  362.                 for key, value in data.items():
  363.                     self.validateTypes(key, [bool, int, long, float, unicode,
  364.                         str, NoneType, datetime, time.struct_time])
  365.                     toValidate.append(value)
  366.             else:
  367.                 self.validateTypes(data, [bool, int, long, float, unicode,str,
  368.                         NoneType, datetime, time.struct_time])
  369.             try:
  370.                 data = toValidate.pop()
  371.             except:
  372.                 data = None
  373.  
  374.  
  375. class ItemSchema(DDBObjectSchema):
  376.     klass = OldItem
  377.     classString = 'item'
  378.     fields = DDBObjectSchema.fields + [
  379.         ('feed', SchemaObject(OldFeed)),
  380.         ('seen', SchemaBool()),
  381.         ('downloaders', SchemaList(SchemaObject(OldRemoteDownloader))),
  382.         ('autoDownloaded', SchemaBool()),
  383.         ('startingDownload', SchemaBool()),
  384.         ('lastDownloadFailed', SchemaBool()),
  385.         ('pendingManualDL', SchemaBool()),
  386.         ('pendingReason', SchemaString()),
  387.         ('entry', SchemaSimpleContainer()),
  388.         ('expired', SchemaBool()),
  389.         ('keep', SchemaBool()),
  390.         ('creationTime', SchemaDateTime()),
  391.         ('linkNumber', SchemaInt(noneOk=True)),
  392.         ('iconCache', SchemaObject(IconCache, noneOk=True)),
  393.         ('downloadedTime', SchemaDateTime(noneOk=True)),
  394.     ]
  395.  
  396. class FileItemSchema(ItemSchema):
  397.     klass = OldFileItem
  398.     classString = 'file-item'
  399.     fields = ItemSchema.fields + [
  400.         ('filename', SchemaString()),
  401.     ]
  402.  
  403. class FeedSchema(DDBObjectSchema):
  404.     klass = OldFeed
  405.     classString = 'feed'
  406.     fields = DDBObjectSchema.fields + [
  407.         ('origURL', SchemaString()),
  408.         ('errorState', SchemaBool()),
  409.         ('initiallyAutoDownloadable', SchemaBool()),
  410.         ('loading', SchemaBool()),
  411.         ('actualFeed', SchemaObject(OldFeedImpl)),
  412.         ('iconCache', SchemaObject(IconCache, noneOk=True)),
  413.     ]
  414.  
  415. class FeedImplSchema(ObjectSchema):
  416.     klass = OldFeedImpl
  417.     classString = 'field-impl'
  418.     fields = [
  419.         ('available', SchemaInt()),
  420.         ('unwatched', SchemaInt()),
  421.         ('url', SchemaString()),
  422.         ('ufeed', SchemaObject(OldFeed)),
  423.         ('items', SchemaList(SchemaObject(OldItem))),
  424.         ('title', SchemaString()),
  425.         ('created', SchemaDateTime()),
  426.         ('autoDownloadable', SchemaBool()),
  427.         ('startfrom', SchemaDateTime()),
  428.         ('getEverything', SchemaBool()),
  429.         ('maxNew', SchemaInt()),
  430.         ('fallBehind', SchemaInt()),
  431.         ('expire', SchemaString()),
  432.         ('visible', SchemaBool()),
  433.         ('updating', SchemaBool()),
  434.         ('lastViewed', SchemaDateTime()),
  435.         ('thumbURL', SchemaString()),
  436.         ('updateFreq', SchemaInt()),
  437.         ('expireTime', SchemaTimeDelta(noneOk=True)),
  438.     ]
  439.  
  440. class RSSFeedImplSchema(FeedImplSchema):
  441.     klass = OldRSSFeedImpl
  442.     classString = 'rss-feed-impl'
  443.     fields = FeedImplSchema.fields + [
  444.         ('initialHTML', SchemaString(noneOk=True)),
  445.         ('etag', SchemaString(noneOk=True)),
  446.         ('modified', SchemaString(noneOk=True)),
  447.     ]
  448.  
  449. class ScraperFeedImplSchema(FeedImplSchema):
  450.     klass = OldScraperFeedImpl
  451.     classString = 'scraper-feed-impl'
  452.     fields = FeedImplSchema.fields + [
  453.         ('initialHTML', SchemaString(noneOk=True)),
  454.         ('initialCharset', SchemaString(noneOk=True)),
  455.         ('linkHistory', SchemaSimpleContainer()),
  456.     ]
  457.  
  458. class SearchFeedImplSchema(FeedImplSchema):
  459.     klass = OldSearchFeedImpl
  460.     classString = 'search-feed-impl'
  461.     fields = FeedImplSchema.fields + [
  462.         ('searching', SchemaBool()),
  463.         ('lastEngine', SchemaString()),
  464.         ('lastQuery', SchemaString()),
  465.     ]
  466.  
  467. class DirectoryFeedImplSchema(FeedImplSchema):
  468.     klass = OldDirectoryFeedImpl
  469.     classString = 'directory-feed-impl'
  470.     # DirectoryFeedImpl doesn't have any addition fields over FeedImpl
  471.  
  472. class SearchDownloadsFeedImplSchema(FeedImplSchema):
  473.     klass = OldSearchDownloadsFeedImpl
  474.     classString = 'search-downloads-feed-impl'
  475.     # SearchDownloadsFeedImpl doesn't have any addition fields over FeedImpl
  476.  
  477. class RemoteDownloaderSchema(DDBObjectSchema):
  478.     klass = OldRemoteDownloader
  479.     classString = 'remote-downloader'
  480.     fields = DDBObjectSchema.fields + [
  481.         ('url', SchemaString()),
  482.         ('itemList', SchemaList(SchemaObject(OldItem))),
  483.         ('dlid', SchemaString()),
  484.         ('contentType', SchemaString(noneOk=True)),
  485.         ('status', SchemaSimpleContainer()),
  486.     ]
  487.  
  488. class HTTPAuthPasswordSchema(DDBObjectSchema):
  489.     klass = OldHTTPAuthPassword
  490.     classString = 'http-auth-password'
  491.     fields = DDBObjectSchema.fields + [
  492.         ('username', SchemaString()),
  493.         ('password', SchemaString()),
  494.         ('host', SchemaString()),
  495.         ('realm', SchemaString()),
  496.         ('path', SchemaString()),
  497.         ('authScheme', SchemaString()),
  498.     ]
  499.  
  500. class FolderSchema(DDBObjectSchema):
  501.     klass = OldFolder
  502.     classString = 'folder'
  503.     fields = DDBObjectSchema.fields + [
  504.         ('feeds', SchemaList(SchemaInt())),
  505.         ('title', SchemaString()),
  506.     ]
  507.  
  508. class ChannelGuideSchema(DDBObjectSchema):
  509.     klass = OldChannelGuide
  510.     classString = 'channel-guide'
  511.     fields = DDBObjectSchema.fields + [
  512.         ('sawIntro', SchemaBool()),
  513.         ('cachedGuideBody', SchemaString(noneOk=True)),
  514.         ('loadedThisSession', SchemaBool()),
  515.     ]
  516.  
  517. objectSchemas = [ 
  518.     DDBObjectSchema, ItemSchema, FileItemSchema, FeedSchema, FeedImplSchema,
  519.     RSSFeedImplSchema, ScraperFeedImplSchema, SearchFeedImplSchema,
  520.     DirectoryFeedImplSchema, SearchDownloadsFeedImplSchema,
  521.     RemoteDownloaderSchema, HTTPAuthPasswordSchema, FolderSchema,
  522.     ChannelGuideSchema, 
  523. ]
  524.  
  525. def convertOldDatabase(databasePath):
  526.     OldItem.allOldItems = set()
  527.     shutil.copyfile(databasePath, databasePath + '.old')
  528.     f = open(databasePath, 'rb')
  529.     p = FakeClassUnpickler(f)
  530.     data = p.load()
  531.     if type(data) == types.ListType:
  532.         # version 0 database
  533.         objects = data
  534.     else:
  535.         # version 1 database
  536.         (version, objects) = data
  537.     # Objects used to be stored as (object, object) tuples.  Remove the dup
  538.     objects = [o[0] for o in objects]
  539.     # drop any top-level DropItLikeItsHot instances
  540.     objects = [o for o in objects if not hasattr(o, '__DropMeLikeItsHot')]
  541.     # Set obj.id for any objects missing it
  542.     idMissing = set()
  543.     lastId = 0
  544.     for o in objects:
  545.         if hasattr(o, 'id'):
  546.             if o.id > lastId:
  547.                 lastId = o.id
  548.         else:
  549.             idMissing.add(o)
  550.     for o in idMissing:
  551.         lastId += 1
  552.         o.id = lastId
  553.     # drop any downloaders that are referenced by items
  554.     def dropItFilter(obj):
  555.         return not hasattr(obj, '__DropMeLikeItsHot')
  556.     for i in OldItem.allOldItems:
  557.         i.downloaders = filter(dropItFilter, i.downloaders)
  558.  
  559.     storedatabase.saveObjectList(objects, databasePath,
  560.             objectSchemas=objectSchemas, version=6)
  561.