PageRenderTime 29ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/hobix/storage/filesys.rb

https://github.com/ecin/hobix
Ruby | 416 lines | 283 code | 36 blank | 97 comment | 30 complexity | 1abc21f6ae5e97530fbef68f9cc86e1c MD5 | raw file
  1. #
  2. # = hobix/storage/filesys.rb
  3. #
  4. # Hobix command-line weblog system.
  5. #
  6. # Copyright (c) 2003-2004 why the lucky stiff
  7. #
  8. # Written & maintained by why the lucky stiff <why@ruby-lang.org>
  9. #
  10. # This program is free software, released under a BSD license.
  11. # See COPYING for details.
  12. #
  13. #--
  14. # $Id$
  15. #++
  16. require 'find'
  17. require 'yaml'
  18. require 'fileutils'
  19. # require 'hobix/search/simple'
  20. require 'hobix/storage/lockfile'
  21. module Hobix
  22. #
  23. # The IndexEntry class
  24. #
  25. class IndexEntry < BaseContent
  26. def initialize( entry, fields = self.class.properties.keys )
  27. fields.each do |field|
  28. val = if entry.respond_to? field
  29. entry.send( field )
  30. elsif respond_to? "make_#{field}"
  31. send( "make_#{field}", entry )
  32. else
  33. :unset
  34. end
  35. send( "#{field}=", val )
  36. end
  37. yield self if block_given?
  38. end
  39. yaml_type "!hobix.com,2004/storage/indexEntry"
  40. end
  41. module Storage
  42. #
  43. # The FileSys class is a storage plugin, it manages the loading and dumping of
  44. # Hobix entries and attachments. The FileSys class also keeps an index of entry
  45. # information, to keep the system from loading unneeded entries.
  46. class FileSys < Hobix::BaseStorage
  47. # Start the storage plugin for the +weblog+ passed in.
  48. def initialize( weblog )
  49. super( weblog )
  50. @updated = {}
  51. @basepath = weblog.entry_path
  52. @default_author = weblog.authors.keys.first
  53. @weblog = weblog
  54. end
  55. def now; Time.at( Time.now.to_i ); end
  56. # The default extension for entries. Defaults to: yaml.
  57. def extension
  58. 'yaml'
  59. end
  60. # Determine if +id+ is a valid entry identifier, untaint if so.
  61. def check_id( id )
  62. id.untaint if id.tainted? and id =~ /^[\w\/\\]+$/
  63. end
  64. # Build an entry's complete path based on its +id+. Optionally, extension +ext+ can
  65. # be used to find the path of attachments.
  66. def entry_path( id, ext = extension )
  67. File.join( @basepath, id.split( '/' ) ) + "." + ext
  68. end
  69. # Brings an entry's updated time current.
  70. def touch_entry( id )
  71. check_id( id )
  72. @updated[id] = Time.now
  73. FileUtils.touch entry_path( id )
  74. end
  75. # Save the entry object +e+ and identify it as +id+. The +create_category+ flag
  76. # will forcefully make the needed directories.
  77. def save_entry( id, e, create_category=false )
  78. load_index
  79. check_id( id )
  80. e.created ||= (@index.has_key?( id ) ? @index[id].created : now)
  81. path = entry_path( id )
  82. unless create_category and File.exists? @basepath
  83. FileUtils.makedirs File.dirname( path )
  84. end
  85. Lockfile.new( path + ".lock" ) do
  86. File.open( path, 'w' ) { |f| YAML::dump( e, f ) }
  87. end
  88. @entry_cache ||= {}
  89. e.id = id
  90. e.link = e.class.url_link e, @link, @weblog.central_ext
  91. e.updated = e.modified = now
  92. @entry_cache[id] = e
  93. @index[id] = @weblog.index_class.new( e ) do |i|
  94. i.updated = e.updated
  95. end
  96. @updated[id] = e.updated
  97. # catalog_search_entry( e )
  98. sort_index( true )
  99. e
  100. end
  101. # Loads the entry object identified by +id+. Entries are cached for future loading.
  102. def load_entry( id )
  103. return default_entry( @default_author ) if id == default_entry_id
  104. load_index
  105. check_id( id )
  106. @entry_cache ||= {}
  107. unless @entry_cache.has_key? id
  108. entry_file = entry_path( id )
  109. e = Hobix::Entry::load( entry_file )
  110. e.id = id
  111. e.link = e.class.url_link e, @link, @weblog.central_ext
  112. e.updated = updated( id )
  113. unless e.created
  114. e.created = @index[id].created
  115. e.modified = @index[id].modified
  116. Lockfile.new( entry_file + ".lock" ) do
  117. File.open( entry_file, 'w' ) { |f| YAML::dump( e, f ) }
  118. end
  119. end
  120. @entry_cache[id] = e
  121. else
  122. @entry_cache[id]
  123. end
  124. end
  125. # Loads the search engine database. The database will be cleansed and re-scanned if +wash+ is true.
  126. # def load_search_index( wash )
  127. # @search_index = Hobix::Search::Simple::Searcher.load( File.join( @basepath, 'index.search' ), wash )
  128. # end
  129. # Catalogs an entry object +e+ in the search engine.
  130. # def catalog_search_entry( e )
  131. # @search_index.catalog( Hobix::Search::Simple::Content.new( e.to_search, e.id, e.modified, e.content_ratings ) )
  132. # end
  133. # Determines if the search engine has already scanned an entry represented by IndexEntry +ie+.
  134. # def search_needs_update? ie
  135. # not @search_index.has_entry? ie.id, ie.modified
  136. # end
  137. # Load the internal index (saved at @entry_path/index.hobix) and refresh any timestamps
  138. # which may be stale.
  139. def load_index
  140. return false if @index
  141. index_path = File.join( @basepath, 'index.hobix' )
  142. index = if File.exists? index_path
  143. YAML::load( File.open( index_path ) )
  144. else
  145. YAML::Omap::new
  146. end
  147. @index = YAML::Omap::new
  148. # load_search_index( index.length == 0 )
  149. modified = false
  150. index_fields = @weblog.index_class.properties.keys
  151. Find::find( @basepath ) do |path|
  152. path.untaint
  153. if FileTest.directory? path
  154. Find.prune if File.basename(path)[0] == ?.
  155. else
  156. entry_path = path.gsub( /^#{ Regexp::quote( @basepath ) }\/?/, '' )
  157. next if entry_path !~ /\.#{ Regexp::quote( extension ) }$/
  158. entry_paths = File.split( $` )
  159. entry_paths.shift if entry_paths.first == '.'
  160. entry_id = entry_paths.join( '/' )
  161. @updated[entry_id] = File.mtime( path )
  162. index_entry = nil
  163. if ( index.has_key? entry_id ) and !( index[entry_id].is_a? ::Time ) # pre-0.4 index format
  164. index_entry = index[entry_id]
  165. end
  166. ## we will (re)load the entry if:
  167. if not index_entry.respond_to?( :updated ) or # it's new
  168. ( index_entry.updated != @updated[entry_id] ) # it's changed
  169. # or index_fields.detect { |f| index_entry.send( f ).nil? } # index fields have been added
  170. # or search_needs_update? index_entry # entry is old or not available in search db
  171. puts "++ Reloaded #{ entry_id }"
  172. efile = entry_path( entry_id )
  173. e = Hobix::Entry::load( efile )
  174. e.id = entry_id
  175. index_entry = @weblog.index_class.new( e, index_fields ) do |i|
  176. i.updated = @updated[entry_id]
  177. end
  178. # catalog_search_entry( e )
  179. modified = true
  180. end
  181. index_entry.id = entry_id
  182. @index[entry_id] = index_entry
  183. end
  184. end
  185. sort_index( modified )
  186. true
  187. end
  188. # Sorts the internal entry index (used by load_index.)
  189. def sort_index( modified )
  190. return unless @index
  191. index_path = File.join( @basepath, 'index.hobix' )
  192. @index.sort! { |x,y| y[1].created <=> x[1].created }
  193. if modified
  194. Lockfile.new( index_path + ".lock" ) do
  195. File.open( index_path, 'w' ) do |f|
  196. YAML::dump( @index, f )
  197. end
  198. end
  199. # @search_index.dump
  200. end
  201. end
  202. # Returns a Hobix::Storage::FileSys object with its scope limited
  203. # to entries inside a certain path +p+.
  204. def path_storage( p )
  205. return self if ['', '.'].include? p
  206. load_index
  207. path_storage = self.dup
  208. path_storage.instance_eval do
  209. @index = @index.dup.delete_if do |id, entry|
  210. if id.index( p ) != 0
  211. @updated.delete( p )
  212. true
  213. end
  214. end
  215. end
  216. path_storage
  217. end
  218. # Returns an Array all `sections', or directories which contain entries.
  219. # If you have three entries: `news/article1', `about/me', and `news/misc/article2',
  220. # then you have three sections: `news', `about', `news/misc'.
  221. def sections( opts = nil )
  222. load_index
  223. hsh = {}
  224. @index.collect { |id, e| e.section_id }.uniq.sort
  225. end
  226. # Find entries based on criteria from the +search+ hash.
  227. # Possible criteria include:
  228. #
  229. # :after:: Select entries created after a given Time.
  230. # :before:: Select entries created before a given Time.
  231. # :inpath:: Select entries contained within a path.
  232. # :match:: Select entries with an +id+ which match a Regexp.
  233. # :search:: Fulltext search of entries for search words.
  234. # :lastn:: Limit the search to include only a given number of entries.
  235. #
  236. # This method returns an Array of +IndexEntry+ objects for use in
  237. # skel_* methods.
  238. def find( search = {} )
  239. load_index
  240. _index = @index
  241. if _index.empty?
  242. e = default_entry( @default_author )
  243. @updated[e.id] = e.updated
  244. _index = {e.id => @weblog.index_class.new(e)}
  245. end
  246. # if search[:search]
  247. # sr = @search_index.find_words( search[:search] )
  248. # end
  249. unless search[:all]
  250. ignore_test = nil
  251. ignored = @weblog.sections_ignored
  252. unless ignored.empty?
  253. ignore_test = /^(#{ ignored.collect { |i| Regexp.quote( i ) }.join( '|' ) })/
  254. end
  255. end
  256. entries = _index.collect do |id, entry|
  257. skip = false
  258. if ignore_test and not search[:all]
  259. skip = entry.id =~ ignore_test
  260. end
  261. search.each do |skey, sval|
  262. break if skip
  263. skip = case skey
  264. when :after
  265. entry.created < sval
  266. when :before
  267. entry.created > sval
  268. when :inpath
  269. entry.id.index( sval ) != 0
  270. when :match
  271. not entry.id.match sval
  272. # when :search
  273. # not sr.results[entry.id]
  274. else
  275. false
  276. end
  277. end
  278. if skip then nil else entry end
  279. end.compact
  280. entries.slice!( search[:lastn]..-1 ) if search[:lastn] and entries.length > search[:lastn]
  281. entries
  282. end
  283. # Returns a Time object for the latest updated time for a group of
  284. # +entries+ (pass in an Array of IndexEntry objects).
  285. def last_updated( entries )
  286. entries.collect do |entry|
  287. updated( entry.id )
  288. end.max
  289. end
  290. # Returns a Time object for the latest modified time for a group of
  291. # +entries+ (pass in an Array of IndexEntry objects).
  292. def last_modified( entries )
  293. entries.collect do |entry|
  294. entry.modified
  295. end.max
  296. end
  297. # Returns a Time object for the latest creation time for a group of
  298. # +entries+ (pass in an Array of IndexEntry objects).
  299. def last_created( entries )
  300. entries.collect do |entry|
  301. entry.created
  302. end.max
  303. end
  304. # Returns a Time object representing the +updated+ time for the
  305. # entry identified by +entry_id+. Takes into account attachments
  306. # which have been updated.
  307. def updated( entry_id )
  308. find_attached( entry_id ).inject( @updated[entry_id] ) do |max, ext|
  309. mtime = File.mtime( entry_path( entry_id, ext ) )
  310. mtime > max ? mtime : max
  311. end
  312. end
  313. # Returns an Array of Arrays representing the months which contain
  314. # +entries+ (pass in an Array of IndexEntry objects).
  315. #
  316. # See Hobix::Weblog.skel_month for an example of this method's usage.
  317. def get_months( entries )
  318. return [] if entries.empty?
  319. first_time = entries.collect { |e| e.created }.min
  320. last_time = entries.collect { |e| e.created }.max
  321. start = Time.mktime( first_time.year, first_time.month, 1 )
  322. stop = Time.mktime( last_time.year, last_time.month, last_time.day )
  323. months = []
  324. until start > stop
  325. next_year, next_month = start.year, start.month + 1
  326. if next_month > 12
  327. next_year += next_month / 12
  328. next_month %= 12
  329. end
  330. month_end = Time.mktime( next_year, next_month, 1 ) - 1
  331. months << [ start, month_end, start.strftime( "/%Y/%m/" ) ] unless find( :after => start, :before => month_end).empty?
  332. start = month_end + 1
  333. end
  334. months
  335. end
  336. # Discovers attachments to an entry identified by +id+.
  337. def find_attached( id )
  338. check_id( id )
  339. Dir[ entry_path( id, '*' ) ].collect do |att|
  340. atp = att.match( /#{ Regexp::quote( id ) }\.(?!#{ extension }$)/ )
  341. atp.post_match if atp
  342. end.compact
  343. end
  344. # Loads an attachment to an entry identified by +id+. Entries
  345. # can have any kind of YAML attachment, each which a specific extension.
  346. def load_attached( id, ext )
  347. check_id( id )
  348. @attach_cache ||= {}
  349. file_id = "#{ id }.#{ ext }"
  350. unless @attach_cache.has_key? file_id
  351. @attach_cache[id] = File.open( entry_path( id, ext ) ) do |f|
  352. YAML::load( f )
  353. end
  354. else
  355. @attach_cache[id]
  356. end
  357. end
  358. # Saves an attachment to an entry identified by +id+. The attachment
  359. # +e+ is saved with an extension +ext+.
  360. def save_attached( id, ext, e )
  361. check_id( id )
  362. Lockfile.new( entry_path( id, ext ) + ".lock" ) do
  363. File.open( entry_path( id, ext ), 'w' ) do |f|
  364. YAML::dump( e, f )
  365. end
  366. end
  367. @attach_cache ||= {}
  368. @attach_cache[id] = e
  369. end
  370. # Appends the given items to an entry attachment with the given type, and
  371. # then saves the modified attachment. If an attachment of the given type
  372. # does not exist, it will be created.
  373. def append_to_attachment( entry_id, attachment_type, *items )
  374. attachment = load_attached( entry_id, attachment_type ) rescue []
  375. attachment += items
  376. save_attached( entry_id, attachment_type, attachment )
  377. end
  378. end
  379. end
  380. end