time left counter was not working
[zip-doc.git] / zarchive.rb
blob474758acdc1741c7947a58a29cb8d829800a7d7b
1 # Library for storing and accessing arbitrary chunks of compressed data.
2 # By Stian Haklev (shaklev@gmail.com), 2007
3 # Released under MIT and GPL licenses
4
5 # Usage example: 
6 # require 'zarchive'
7 # archive = ZArchive::Writer.new('eo.zdump')
8 # index = File.read('index.html')
9 # archive.add('index.html', index)
10 # archive.add_hardlink('index.htm', 'index.html)
11 # archive.flush
12
13 # archive = ZArchive::Reader.new('eo.zdump')
14 # puts(archive.get('index.html))
16 %w(sha1 zutil).each {|x| require x} 
18 module ZArchive
19   METHOD_BZ2 = 1
20   METHOD_ZLIB = 2
22   class Compressor
23     # methods are bz2 and zlib
24     attr_reader :method
26     def initialize(method)
27       @method = method
28       require (@method == METHOD_BZ2 ? 'bz2' : 'zlib')
29     end
31     def uncompress(txt)
32       case @method   
33       when METHOD_BZ2 : BZ2::Reader.new(txt).read
34       when METHOD_ZLIB : Zlib::Inflate.new.inflate(txt)  
35       end            
36     end
38     # compresses a textchunk, that is able to be uncompressed independently
39     def compress(txt)
40       case @method
41       when METHOD_BZ2 : (BZ2::Writer.new << txt).flush
42       when METHOD_ZLIB : Zlib::Deflate.new.deflate(txt, Zlib::FINISH)      
43       end
44     end
45   end
47   class Reader               
48     include ZUtil
49     def initialize(file)
50       @file = file                    
52       zdump = File.open(@file, 'r')
53       @zindex_loc, @meta_loc, @compress, idx_size = zdump.read(12).unpack('VVCC')
54       @idx_size = idx_size
55       @compressor = Compressor.new(@compress)
56     end
58     def get(url)
59       # we open this on each request, because otherwise it gets messy with threading
60       zdump = File.open(@file, 'r')
62       loc = get_location(url, zdump, @zindex_loc)
63       return loc ? get_text(zdump, *loc) : nil
64     end
66     def get_text(zdump, block_offset, block_size, offset, size)
67       text_compr = readloc( zdump, block_size, block_offset )
68       text_uncompr = @compressor.uncompress( text_compr )
69       return text_uncompr[offset, size]
70     end
72     def get_meta
73       zdump = File.open(@file, 'r')
74       zdump.seek(@meta_loc)
75       Marshal.load(zdump.read)
76     end
78     def get_location(url, zdump, zindex_loc)
79       sha1, firstfour = sha1_w_sub(url, @idx_size)
81       # uses this number to calculate the location of the metaindex entry
82       loc = (firstfour * 8) + zindex_loc                            
83       
84       # finds the location of the index entry
85       start, size = readloc(zdump, 8, loc).unpack('V2')
86       idx = readloc(zdump, size, start)
87       
88       # the index consists of a number of 36 byte entries. it sorts through
89       # until it finds the right one.
90       
91       return if idx.empty?
92       hex, *coordinates = idx.pop(36).unpack('H40V4') until ( hex == sha1 || idx.nil? )
93       return coordinates if hex == sha1
94     end
95   end   
97   class Writer
98     include ZUtil
99     attr_reader :location, :hardlinks
101     @@entry = Struct.new(:uri, :block, :buflocation, :size, :sha1)
102     @@block = Struct.new(:number, :start, :size, :pages)                         
104     # the uri to open, the minimum size of blocks, and zlib or bz2
105     def initialize(file, method = METHOD_BZ2, idx_size = 4, blocksize = 900000)
106       @compressor = Compressor.new(method)
107       @blocksize = blocksize
108       @file = File.open(file, "w")
109       @index = []         
110       @cur_block, @buflocation, @size = 0, 0, 0
111       @buffer = ''
112       @location = 12 # (to hold start of index)
113       @block_ary = [] 
114       @hardlinks = {}
115       @idx_size = idx_size
116     end
118     # adds a blob of text that will be acessible through a certain uri
119     def add(uri, text)
120       # if redirect, add to index and keep going
121       entry = @@entry.new(uri, @cur_block, @buflocation, text.size)
123       # calculate the sha1 code, use the first four characters as the index
124       entry.sha1, firstfour = sha1_w_sub(entry.uri, @idx_size)
126       # add this entry to the index in the right place
127       @index[firstfour] ||= []
128       @index[firstfour] << entry
130       # add to the buffer, and update the counter
131       @buffer << text
132       @buflocation += text.size
134       flush_block if @buffer.size > @blocksize
135     end
137     # hardlinks the contents of one uri to that of another
138     def add_hardlink(uri, targeturi)
139       @hardlinks[uri] = targeturi
140     end
142     def set_meta(meta)
143       @meta = meta
144     end
146     # finish up, process hardlinks, and write index to file
147     def flush
148       flush_block unless @buffer.empty?     
149       process_hardlinks
151       # writing the location of the archive (it's after the dump data)
152       writeloc(@file, [@location].pack('V'), 0)                      
154       indexloc = @location
155       location =  (sha1subset('FFFFFFFFFF', @idx_size) * 8) + indexloc
156       # p = File.open("zlog", "w")
157       each_entry_with_index do |entry, idx|
158         next if entry.nil?  
160         writeloc(@file, [location, entry.size].pack('V2'), (idx * 8) + indexloc)
161         writeloc(@file, entry, location)
163         # p << "*" * 80 << "\n" 
164         # p << "seek #{(idx * 8) + indexloc} location #{location} size #{entry.size}" << "\n"
165         # p << unpack(entry).join(":") << "\n"
167         location += entry.size
168       end
170       # meta location
171       writeloc(@file, [location, @compressor.method, @idx_size].pack('VCC'), 4)
173       writeloc(@file, Marshal.dump(@meta), @location) if defined?(@meta)
175       @file.close
176     end
178     private
179     # yields an entry that is ready to be written to the index
180     def each_entry_with_index
181       @index.each_with_index do |hash, idx|
182         next if hash.nil?
183         entry = ''  
184         hash.each {|x| entry << pack(x.sha1, @block_ary[x.block].start, @block_ary[x.block].size, x.buflocation, x.size) }
185         yield entry, idx  
186       end
187     end
189     # must be run after all the uris have been added, so their coordinates are known
190     # adds entries for the hardlinks into the main index
191     def process_hardlinks
192       counter = 0
193       @hardlinks.each do |file, target|
194         counter += 1  
196         # in case of recursive redirects, which shouldn't happen, but alas
197         recursion = 0
198         while @hardlinks[target] && recursion < 3
199           recursion += 1
200           target = @hardlinks[target]
201         end
203         # we'll just traverse the index and fetch the coords of the target
204         sha1, firstfour = sha1_w_sub(file)
205         sha1_target, firstfour_target = sha1_w_sub(target)
207         entries = @index[firstfour_target]
208         next if entries.nil?
210         target = entries.select {|entry| entry.sha1 == sha1_target}
212         # it really shouldn't be empty... if it is - the redirect is useless
213         # anyway
214         unless target.empty?         
215           entry = target[0].dup  # so we don't overwrite the original
217           # we just reuse the same entry, rewrite the sha1, and add it to the index
218           entry.sha1 = sha1
219           @index[firstfour] ||= []        
220           @index[firstfour] << entry
221         end
223       end
224       @hardlinks = nil   # clean up some memory
225     end
227     # output the block in buffer to file, store the coords, and clean the buffer
228     def flush_block
229       bf_compr = @compressor.compress(@buffer)
230       writeloc(@file, bf_compr, @location)
231       @block_ary[@cur_block] = @@block.new(@cur_block, @location, bf_compr.size)
233       @buffer = ''       
234       @buflocation = 0
235       @cur_block += 1                                           
236       @location += bf_compr.size
237     end  
238   end
239 end