File: file_system.rb

package info (click to toggle)
libole-ruby 1.2.9-4
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 596 kB
  • ctags: 401
  • sloc: ruby: 2,763; makefile: 13
file content (423 lines) | stat: -rw-r--r-- 12,088 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
#
# = Introduction
#
# This file intends to provide file system-like api support, a la <tt>zip/zipfilesystem</tt>.
#
# = TODO
# 
# - need to implement some more IO functions on RangesIO, like #puts, #print
#   etc, like AbstractOutputStream from zipfile.
#
# - check Dir.mkdir, and File.open, and File.rename, to add in filename 
#   length checks (max 32 / 31 or something).
#   do the automatic truncation, and add in any necessary warnings.
#
# - File.split('a/') == File.split('a') == ['.', 'a']
#   the implication of this, is that things that try to force directory
#   don't work. like, File.rename('a', 'b'), should work if a is a file
#   or directory, but File.rename('a/', 'b') should only work if a is
#   a directory. tricky, need to clean things up a bit more.
#   i think a general path name => dirent method would work, with flags
#   about what should raise an error. 
#
# - Need to look at streamlining things after getting all the tests passing,
#   as this file's getting pretty long - almost half the real implementation.
#   and is probably more inefficient than necessary.
#   too many exceptions in the expected path of certain functions.
#
# - should look at profiles before and after switching ruby-msg to use
#   the filesystem api.
#

require 'ole/storage'

module Ole # :nodoc:
	class Storage
		def file
			@file ||= FileClass.new self
		end

		def dir
			@dir ||= DirClass.new self
		end

		# tries to get a dirent for path. return nil if it doesn't exist
		# (change it)
		def dirent_from_path path
			dirent = @root
			path = file.expand_path path
			path = path.sub(/^\/*/, '').sub(/\/*$/, '').split(/\/+/)
			until path.empty?
				return nil if dirent.file?
				return nil unless dirent = dirent/path.shift
			end
			dirent
		end

		class FileClass
			class Stat
				attr_reader :ftype, :size, :blocks, :blksize
				attr_reader :nlink, :uid, :gid, :dev, :rdev, :ino
				def initialize dirent
					@dirent = dirent
					@size = dirent.size
					if file?
						@ftype = 'file'
						bat = dirent.ole.bat_for_size(dirent.size)
						@blocks = bat.chain(dirent.first_block).length
						@blksize = bat.block_size
					else
						@ftype = 'directory'
						@blocks = 0
						@blksize = 0
					end
					# a lot of these are bogus. ole file format has no analogs
					@nlink = 1
					@uid, @gid = 0, 0
					@dev, @rdev = 0, 0
					@ino = 0
					# need to add times - atime, mtime, ctime. 
				end

				alias rdev_major :rdev
				alias rdev_minor :rdev

				def file?
					@dirent.file?
				end

				def directory?
					@dirent.dir?
				end
				
				def size?
					size if file?
				end

				def inspect
					pairs = (instance_variables - ['@dirent']).map do |n|
						"#{n[1..-1]}=#{instance_variable_get n}"
					end
					"#<#{self.class} #{pairs * ', '}>"
				end
			end

			def initialize ole
				@ole = ole
			end

			def expand_path path
				# get the raw stored pwd value (its blank for root)
				pwd = @ole.dir.instance_variable_get :@pwd
				# its only absolute if it starts with a '/'
				path = "#{pwd}/#{path}" unless path =~ /^\//
				# at this point its already absolute. we use File.expand_path
				# just for the .. and . handling
				# No longer use RUBY_PLATFORM =~ /win/ as it matches darwin. better way?
				File.expand_path(path)[File::ALT_SEPARATOR == "\\" ? (2..-1) : (0..-1)]
			end

			# +orig_path+ is just so that we can use the requested path
			# in the error messages even if it has been already modified
			def dirent_from_path path, orig_path=nil
				orig_path ||= path
				dirent = @ole.dirent_from_path path
				raise Errno::ENOENT,  orig_path unless dirent
				raise Errno::EISDIR, orig_path if dirent.dir?
				dirent
			end
			private :dirent_from_path

			def exists? path
				!!@ole.dirent_from_path(path)
			end
			alias exist? :exists?

			def file? path
				dirent = @ole.dirent_from_path path
				dirent and dirent.file?
			end

			def directory? path
				dirent = @ole.dirent_from_path path
				dirent and dirent.dir?
			end

			def open path, mode='r', &block
				if IO::Mode.new(mode).create?
					begin
						dirent = dirent_from_path path
					rescue Errno::ENOENT
						# maybe instead of repeating this everywhere, i should have
						# a get_parent_dirent function.
						parent_path, basename = File.split expand_path(path)
						parent = @ole.dir.send :dirent_from_path, parent_path, path
						parent.children << dirent = Dirent.new(@ole, :type => :file, :name => basename)
					end
				else
					dirent = dirent_from_path path
				end
				dirent.open mode, &block
			end

			# explicit wrapper instead of alias to inhibit block
			def new path, mode='r'
				open path, mode
			end

			def size path
				dirent_from_path(path).size
			rescue Errno::EISDIR
				# kind of arbitrary. I'm getting 4096 from ::File, but
				# the zip tests want 0.
				0
			end
			
			def size? path
				dirent_from_path(path).size
				# any other exceptions i need to rescue?
			rescue Errno::ENOENT, Errno::EISDIR
				nil
			end

			def stat path
				# we do this to allow dirs.
				dirent = @ole.dirent_from_path path
				raise Errno::ENOENT, path unless dirent
				Stat.new dirent
			end

			def read path
				open path, &:read
			end

			# most of the work this function does is moving the dirent between
			# 2 parents. the actual name changing is quite simple.
			# File.rename can move a file into another folder, which is why i've
			# done it too, though i think its not always possible...
			#
			# FIXME File.rename can be used for directories too....
			def rename from_path, to_path
				# check what we want to rename from exists. do it this
				# way to allow directories.
				dirent = @ole.dirent_from_path from_path
				raise Errno::ENOENT, from_path unless dirent
				# delete what we want to rename to if necessary
				begin
					unlink to_path
				rescue Errno::ENOENT
					# we actually get here, but rcov doesn't think so. add 1 + 1 to
					# keep rcov happy for now... :)
					1 + 1
				end
				# reparent the dirent
				from_parent_path, from_basename = File.split expand_path(from_path)
				to_parent_path, to_basename = File.split expand_path(to_path)
				from_parent = @ole.dir.send :dirent_from_path, from_parent_path, from_path
				to_parent = @ole.dir.send :dirent_from_path, to_parent_path, to_path
				from_parent.children.delete dirent
				# and also change its name
				dirent.name = to_basename
				to_parent.children << dirent
				0
			end

			# crappy copy from Dir.
			def unlink(*paths)
				paths.each do |path|
					dirent = @ole.dirent_from_path path
					# i think we should free all of our blocks from the
					# allocation table.
					# i think if you run repack, all free blocks should get zeroed,
					# but currently the original data is there unmodified.
					open(path) { |f| f.truncate 0 }
					# remove ourself from our parent, so we won't be part of the dir
					# tree at save time.
					parent_path, basename = File.split expand_path(path)
					parent = @ole.dir.send :dirent_from_path, parent_path, path
					parent.children.delete dirent
				end
				paths.length # hmmm. as per ::File ?
			end
			alias delete :unlink
		end

		#
		# an *instance* of this class is supposed to provide similar methods
		# to the class methods of Dir itself.
		#
		# pretty complete. like zip/zipfilesystem's implementation, i provide
		# everything except chroot and glob. glob could be done with a glob
		# to regex regex, and then simply match in the entries array... although
		# recursive glob complicates that somewhat.
		#
		# Dir.chroot, Dir.glob, Dir.[], and Dir.tmpdir is the complete list.
		class DirClass
			def initialize ole
				@ole = ole
				@pwd = ''
			end

			# +orig_path+ is just so that we can use the requested path
			# in the error messages even if it has been already modified
			def dirent_from_path path, orig_path=nil
				orig_path ||= path
				dirent = @ole.dirent_from_path path
				raise Errno::ENOENT,  orig_path unless dirent
				raise Errno::ENOTDIR, orig_path unless dirent.dir?
				dirent
			end
			private :dirent_from_path

			def open path
				dir = Dir.new path, entries(path)
				if block_given?
					yield dir
				else
					dir
				end
			end

			# as for file, explicit alias to inhibit block
			def new path
				open path
			end

			# pwd is always stored without the trailing slash. we handle
			# the root case here
			def pwd
				if @pwd.empty?
					'/'
				else
					@pwd
				end
			end
			alias getwd :pwd

			def chdir orig_path
				# make path absolute, squeeze slashes, and remove trailing slash
				path = @ole.file.expand_path(orig_path).gsub(/\/+/, '/').sub(/\/$/, '')
				# this is just for the side effects of the exceptions if invalid
				dirent_from_path path, orig_path
				if block_given?
					old_pwd = @pwd
					begin
						@pwd = path
						yield
					ensure
						@pwd = old_pwd
					end
				else
					@pwd = path
					0
				end
			end	

			def entries path
				dirent = dirent_from_path path
				# Not sure about adding on the dots...
				entries = %w[. ..] + dirent.children.map(&:name)
				# do some checks about un-reachable files
				seen = {}
				entries.each do |n|
					Log.warn "inaccessible file (filename contains slash) - #{n.inspect}" if n['/']
					Log.warn "inaccessible file (duplicate filename) - #{n.inspect}" if seen[n]
					seen[n] = true
				end
				entries
			end

			def foreach path, &block
				entries(path).each(&block)
			end

			# there are some other important ones, like:
			# chroot (!), glob etc etc. for now, i think
			def mkdir path
				# as for rmdir below:
				parent_path, basename = File.split @ole.file.expand_path(path)
				# note that we will complain about the full path despite accessing
				# the parent path. this is consistent with ::Dir
				parent = dirent_from_path parent_path, path
				# now, we first should ensure that it doesn't already exist
				# either as a file or a directory.
				raise Errno::EEXIST, path if parent/basename
				parent.children << Dirent.new(@ole, :type => :dir, :name => basename)
				0
			end

			def rmdir path
				dirent = dirent_from_path path
				raise Errno::ENOTEMPTY, path unless dirent.children.empty?

				# now delete it, how to do that? the canonical representation that is
				# maintained is the root tree, and the children array. we must remove it
				# from the children array.
				# we need the parent then. this sucks but anyway:
				# we need to split the path. but before we can do that, we need
				# to expand it first. eg. say we need the parent to unlink
				# a/b/../c. the parent should be a, not a/b/.., or a/b.
				parent_path, basename = File.split @ole.file.expand_path(path)
				# this shouldn't be able to fail if the above didn't
				parent = dirent_from_path parent_path
				# note that the way this currently works, on save and repack time this will get
				# reflected. to work properly, ie to make a difference now it would have to re-write
				# the dirent. i think that Ole::Storage#close will handle that. and maybe include a
				# #repack.
				parent.children.delete dirent
				0 # hmmm. as per ::Dir ?
			end
			alias delete :rmdir
			alias unlink :rmdir

			# note that there is nothing remotely ole specific about
			# this class. it simply provides the dir like sequential access
			# methods on top of an array.
			# hmm, doesn't throw the IOError's on use of a closed directory...
			class Dir
				include Enumerable

				attr_reader :path
				def initialize path, entries
					@path, @entries, @pos = path, entries, 0
					@closed = false
				end
				
				def pos
					raise IOError if @closed
					@pos
				end

				def each(&block)
					raise IOError if @closed
					@entries.each(&block)
				end

				def close
					@closed = true
				end

				def read
					raise IOError if @closed
					@entries[pos]
				ensure
					@pos += 1 if pos < @entries.length
				end

				def pos= pos
					raise IOError if @closed
					@pos = [[0, pos].max, @entries.length].min
				end

				def rewind
					raise IOError if @closed
					@pos = 0
				end

				alias tell :pos
				alias seek :pos=
			end
		end
	end
end