cotta 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +75 -0
- data/lib/cotta.rb +12 -0
- data/lib/cotta/cotta.rb +37 -0
- data/lib/cotta/cotta_dir.rb +184 -0
- data/lib/cotta/cotta_file.rb +253 -0
- data/lib/cotta/file_factory.rb +117 -0
- data/lib/cotta/file_not_found_error.rb +13 -0
- data/lib/cotta/impl/command_error.rb +13 -0
- data/lib/cotta/impl/command_interface.rb +46 -0
- data/lib/cotta/impl/command_runner.rb +55 -0
- data/lib/cotta/impl/cotta_pathname.rb +9 -0
- data/lib/cotta/impl/in_memory_system.rb +295 -0
- data/lib/cotta/impl/physical_system.rb +90 -0
- data/lib/cotta/version +1 -0
- data/test/cotta/command_interface_spec.rb +42 -0
- data/test/cotta/command_runner_spec.rb +24 -0
- data/test/cotta/content.txt +3 -0
- data/test/cotta/cotta_dir_behaviors.rb +202 -0
- data/test/cotta/cotta_dir_in_memory_spec.rb +23 -0
- data/test/cotta/cotta_dir_physical_spec.rb +14 -0
- data/test/cotta/cotta_file_behaviors.rb +139 -0
- data/test/cotta/cotta_file_in_memory_spec.rb +15 -0
- data/test/cotta/cotta_file_physical_spec.rb +47 -0
- data/test/cotta/cotta_zip_support_spec.rb +53 -0
- data/test/cotta/example_spec.rb +20 -0
- data/test/cotta/file_factory_spec.rb +54 -0
- data/test/cotta/file_system_behaviors.rb +163 -0
- data/test/cotta/in_memory_system_spec.rb +24 -0
- data/test/cotta/logo.gif +0 -0
- data/test/cotta/pathname_spec.rb +20 -0
- data/test/cotta/physical_system_spec.rb +34 -0
- data/test/cotta/physical_system_stub.rb +122 -0
- data/test/cotta/tar_test.tar +0 -0
- data/test/test.rb +19 -0
- data/test/ts_cotta.rb +6 -0
- metadata +89 -0
data/README
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
Welcome to the Ruby version of the {Cotta file API}[http://cotta.rubyforge.org]
|
2
|
+
|
3
|
+
= Introduction
|
4
|
+
|
5
|
+
Cotta project is created to provide a lightweight, simple and sensible API to file operation and testing.
|
6
|
+
See {Cotta Power}[http://cotta.sourceforge.net/power.html] for its motivation
|
7
|
+
|
8
|
+
|
9
|
+
= Install It
|
10
|
+
|
11
|
+
The easiest way to install the install cotta using RubyGems:
|
12
|
+
|
13
|
+
sudo gem install cotta
|
14
|
+
|
15
|
+
= Features
|
16
|
+
|
17
|
+
Cotta is just a plain Ruby API, so you can use it wherever you can use Ruby.
|
18
|
+
|
19
|
+
To used the new API just require the client driver:
|
20
|
+
|
21
|
+
require "rubygems"
|
22
|
+
require "cotta"
|
23
|
+
|
24
|
+
For a fully backward compatible API you can start with:
|
25
|
+
|
26
|
+
require "rubygems"
|
27
|
+
gem "cotta"
|
28
|
+
require "cotta"
|
29
|
+
|
30
|
+
For instance
|
31
|
+
to write a little Ruby script using cotta you could write something like:
|
32
|
+
|
33
|
+
#!/usr/bin/env ruby
|
34
|
+
#
|
35
|
+
# Sample Ruby script using the Cotta API
|
36
|
+
#
|
37
|
+
require "rubygems"
|
38
|
+
gem "cotta", ">=1.0.0"
|
39
|
+
require "cotta"
|
40
|
+
|
41
|
+
#system implementation is injected here
|
42
|
+
cotta = Cotta.physical
|
43
|
+
file = cotta.file('dir/file.txt')
|
44
|
+
file.should_not be_exists
|
45
|
+
# parent directories are created automatically
|
46
|
+
file.save('my content')
|
47
|
+
file2 = cotta.file('dir/file2.txt')
|
48
|
+
file2.should_not be_exists
|
49
|
+
file.copy_to(file2)
|
50
|
+
file2.should be_exists
|
51
|
+
file2.load.should == 'my content'
|
52
|
+
file2.read {|file| puts file.gets}
|
53
|
+
|
54
|
+
= Writing Tests
|
55
|
+
|
56
|
+
To test your code that uses Cotta API, you just need to pass in a Cotta instance that is backed by an in-memory
|
57
|
+
file system:
|
58
|
+
|
59
|
+
cotta = Cotta.in_memory
|
60
|
+
|
61
|
+
= Resources
|
62
|
+
|
63
|
+
* Source Code at http://github.com/wolfdancer/cotta
|
64
|
+
* Report bugs at http://github.com/wolfdancer/cotta/issues
|
65
|
+
* Browse API at http://cotta.rubyforge.org
|
66
|
+
* Discuss at http://groups.google.com/group/cotta
|
67
|
+
|
68
|
+
= Reports
|
69
|
+
|
70
|
+
* rSpec: {rspec}[link:rspec/index.html]
|
71
|
+
* code coverage:{rcov}[link:rcov/index.html]
|
72
|
+
|
73
|
+
= Team
|
74
|
+
|
75
|
+
* Shane Duan
|
data/lib/cotta.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require File.dirname(__FILE__) + '/cotta/cotta'
|
3
|
+
require File.dirname(__FILE__) + '/cotta/cotta_dir'
|
4
|
+
require File.dirname(__FILE__) + '/cotta/cotta_file'
|
5
|
+
require File.dirname(__FILE__) + '/cotta/impl/cotta_pathname'
|
6
|
+
require File.dirname(__FILE__) + '/cotta/impl/in_memory_system'
|
7
|
+
require File.dirname(__FILE__) + '/cotta/impl/physical_system'
|
8
|
+
require File.dirname(__FILE__) + '/cotta/impl/command_error'
|
9
|
+
require File.dirname(__FILE__) + '/cotta/impl/command_interface'
|
10
|
+
require File.dirname(__FILE__) + '/cotta/file_not_found_error'
|
11
|
+
require File.dirname(__FILE__) + '/cotta/impl/command_runner'
|
12
|
+
require File.dirname(__FILE__) + '/cotta/file_factory'
|
data/lib/cotta/cotta.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Cotta module that contains all the classes used for file operations
|
2
|
+
# see link:files/README.html
|
3
|
+
# There are also four methods on the module for the most common ways to
|
4
|
+
# start with Cotta API
|
5
|
+
module Cotta
|
6
|
+
# Creates CottaFile repersenting physical file
|
7
|
+
def self::file(path)
|
8
|
+
FileFactory.file(path)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Creates CottaDir representing physical directory
|
12
|
+
def self::dir(path)
|
13
|
+
FileFactory.dir(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Creates CottaDir that is the parent of the path
|
17
|
+
# This is typically used with __FILE__
|
18
|
+
# e.g. dir = Cotta.parent_dir(__FILE__)
|
19
|
+
def self.parent_dir(path)
|
20
|
+
FileFactory.parent_dir(path)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the file facotry backed by physical system
|
24
|
+
def self::physical
|
25
|
+
FileFactory.physical
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the file factory backed by in-memory system
|
29
|
+
def self::in_memory
|
30
|
+
FileFactory.in_memory
|
31
|
+
end
|
32
|
+
|
33
|
+
# returns the file factory backed by the given system
|
34
|
+
def self::factory(system)
|
35
|
+
FileFactory.new(system)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Cotta
|
2
|
+
# This class represents a directory
|
3
|
+
class CottaDir
|
4
|
+
# Path of the directory
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
# file factory of the directory
|
8
|
+
attr_reader :factory
|
9
|
+
|
10
|
+
# Create an instance of CottaDir that is on
|
11
|
+
# the given path and
|
12
|
+
# backed by the given system
|
13
|
+
def initialize(factory, path)
|
14
|
+
@path = path
|
15
|
+
@factory = factory
|
16
|
+
@name = @path.basename.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
# name of the directory
|
20
|
+
def name
|
21
|
+
name = nil
|
22
|
+
if root?
|
23
|
+
name = path.to_s
|
24
|
+
else
|
25
|
+
name = @path.basename.to_s
|
26
|
+
end
|
27
|
+
return name
|
28
|
+
end
|
29
|
+
|
30
|
+
# returns true if this directory is the root directory
|
31
|
+
def root?
|
32
|
+
parent.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# returns true if this directory exists
|
36
|
+
def exists?
|
37
|
+
factory.system.dir_exists?(@path)
|
38
|
+
end
|
39
|
+
|
40
|
+
# returns the stat of the current directory
|
41
|
+
def stat
|
42
|
+
factory.system.dir_stat(@path)
|
43
|
+
end
|
44
|
+
|
45
|
+
# returns the parent directory of this directory
|
46
|
+
# or nil if this is root
|
47
|
+
def parent
|
48
|
+
parent_path = @path.cotta_parent
|
49
|
+
return nil unless parent_path
|
50
|
+
candidate = CottaDir.new(factory, parent_path)
|
51
|
+
if (block_given?)
|
52
|
+
candidate = candidate.parent until candidate.nil? or yield candidate
|
53
|
+
end
|
54
|
+
candidate
|
55
|
+
end
|
56
|
+
|
57
|
+
# returns the relative path from the given file or directory
|
58
|
+
def relative_path_from(entry)
|
59
|
+
@path.relative_path_from(entry.path)
|
60
|
+
end
|
61
|
+
|
62
|
+
# returns the sub-directory with the given name
|
63
|
+
def dir(name)
|
64
|
+
return CottaDir.new(factory, @path.join(name))
|
65
|
+
end
|
66
|
+
|
67
|
+
# returns the file under this directory with the given name
|
68
|
+
def file(name)
|
69
|
+
return CottaFile.new(factory, @path.join(name))
|
70
|
+
end
|
71
|
+
|
72
|
+
# creates this directory and its parent directory
|
73
|
+
def mkdirs
|
74
|
+
if (not exists?)
|
75
|
+
parent.mkdirs
|
76
|
+
factory.system.mkdir @path
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# deletes this directory and all its children
|
81
|
+
def delete
|
82
|
+
if (exists?)
|
83
|
+
list.each do |children|
|
84
|
+
children.delete
|
85
|
+
end
|
86
|
+
factory.system.delete_dir(@path)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# move this directory to target directory
|
91
|
+
# this method assumes that this directory and the target directory
|
92
|
+
# are backed by the same file system
|
93
|
+
def move_to(target)
|
94
|
+
target.parent.mkdirs
|
95
|
+
factory.system.move_dir(@path, target.path)
|
96
|
+
end
|
97
|
+
|
98
|
+
# move this directory to target path
|
99
|
+
# this method assumes that this directory and the target directory
|
100
|
+
# are backed by the same file system
|
101
|
+
def move_to_path(target_path)
|
102
|
+
move_to(cotta.dir(target_path))
|
103
|
+
end
|
104
|
+
|
105
|
+
# copy this directory to target directory
|
106
|
+
# this method assumes that this directory and the target directory
|
107
|
+
# are backed by the same file system
|
108
|
+
def copy_to(target)
|
109
|
+
target.parent.mkdirs
|
110
|
+
factory.system.copy_dir(@path, target.path)
|
111
|
+
end
|
112
|
+
|
113
|
+
# archive this directory and call the given block
|
114
|
+
# to determine if a file or directory should be included
|
115
|
+
def archive(target = nil, &block)
|
116
|
+
require 'rubygems/package'
|
117
|
+
unless target
|
118
|
+
target = parent.file("#{name}.tar")
|
119
|
+
end
|
120
|
+
target.write_binary do |io|
|
121
|
+
writer = Gem::Package::TarWriter.new(io) do |tar_io|
|
122
|
+
archive_dir(tar_io, self, &block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
target
|
126
|
+
end
|
127
|
+
|
128
|
+
def archive_dir(tar_io, dir, &block)
|
129
|
+
dir.list.each do |child|
|
130
|
+
if (block_given? and not yield child)
|
131
|
+
next
|
132
|
+
end
|
133
|
+
stat = child.stat
|
134
|
+
entry_name = child.relative_path_from(self).to_s
|
135
|
+
mode = stat.mode
|
136
|
+
if (stat.file?)
|
137
|
+
tar_io.add_file(entry_name, mode) do |entry_output|
|
138
|
+
child.read_binary do |input|
|
139
|
+
CottaFile.copy_io(input, entry_output)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
elsif (stat.directory?)
|
143
|
+
tar_io.mkdir(entry_name, mode)
|
144
|
+
archive_dir(tar_io, child, &block)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
private :archive_dir
|
150
|
+
|
151
|
+
def copy_to_path(target_path)
|
152
|
+
copy_to(cotta.dir(target_path))
|
153
|
+
end
|
154
|
+
|
155
|
+
# returns the content of this directory
|
156
|
+
# as an array of CottaFile and CottaDirectory
|
157
|
+
def list
|
158
|
+
factory.system.list(@path).collect do |item|
|
159
|
+
candidate = dir(item)
|
160
|
+
if (not candidate.exists?)
|
161
|
+
candidate = file(item)
|
162
|
+
end
|
163
|
+
candidate
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def chdir(&block)
|
168
|
+
factory.system.chdir(@path, &block)
|
169
|
+
end
|
170
|
+
|
171
|
+
def ==(other)
|
172
|
+
return @path == other.path && factory.system == other.factory.system
|
173
|
+
end
|
174
|
+
|
175
|
+
def inspect
|
176
|
+
return "#{self.class}:#{self.object_id}-#@path"
|
177
|
+
end
|
178
|
+
|
179
|
+
def to_s
|
180
|
+
@path.to_s
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Cotta
|
4
|
+
# This class represents a file
|
5
|
+
class CottaFile
|
6
|
+
# factory with the backing system
|
7
|
+
attr_reader :factory
|
8
|
+
# path of this file
|
9
|
+
attr_reader :path
|
10
|
+
# stats of this file
|
11
|
+
attr_reader :stat
|
12
|
+
|
13
|
+
# creates an instance of file with the given factory and path
|
14
|
+
def initialize(factory, path)
|
15
|
+
@path = path
|
16
|
+
@factory = factory
|
17
|
+
end
|
18
|
+
|
19
|
+
# name of this file
|
20
|
+
def name
|
21
|
+
return @path.basename.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
# extension of this file, with '.'
|
25
|
+
def extname
|
26
|
+
return @path.extname
|
27
|
+
end
|
28
|
+
|
29
|
+
# basename of this file
|
30
|
+
def basename
|
31
|
+
return @path.basename(extname).to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
# stats of this file
|
35
|
+
def stat
|
36
|
+
factory.system.file_stat(@path)
|
37
|
+
end
|
38
|
+
|
39
|
+
# returns true if this file is older than the given file
|
40
|
+
def older_than?(file)
|
41
|
+
(stat <=> file.stat) == -1
|
42
|
+
end
|
43
|
+
|
44
|
+
# returns true if this file exists
|
45
|
+
def exists?
|
46
|
+
return factory.system.file_exists?(@path)
|
47
|
+
end
|
48
|
+
|
49
|
+
# returns the relative path from the given file or directory
|
50
|
+
def relative_path_from(entry)
|
51
|
+
path.relative_path_from(entry.path)
|
52
|
+
end
|
53
|
+
|
54
|
+
# returns the parent directory
|
55
|
+
def parent
|
56
|
+
return CottaDir.new(factory, @path.parent)
|
57
|
+
end
|
58
|
+
|
59
|
+
# copy this file to the target file
|
60
|
+
def copy_to(target_file)
|
61
|
+
target_file.parent.mkdirs
|
62
|
+
factory.system.copy_file(path, target_file.path)
|
63
|
+
target_file
|
64
|
+
end
|
65
|
+
|
66
|
+
# copy this file to the target path
|
67
|
+
def copy_to_path(target_path)
|
68
|
+
copy_to(cotta.file(target_path))
|
69
|
+
end
|
70
|
+
|
71
|
+
# move this file to the target file
|
72
|
+
def move_to(target_file)
|
73
|
+
target_file.parent.mkdirs
|
74
|
+
factory.system.move_file(path, target_file.path)
|
75
|
+
end
|
76
|
+
|
77
|
+
# move this file to the target path
|
78
|
+
def move_to_path(target_path)
|
79
|
+
move_to(cotta.file(target_path))
|
80
|
+
end
|
81
|
+
|
82
|
+
# save conent to this file
|
83
|
+
# this will create the parent directory if necessary
|
84
|
+
def save(content = '')
|
85
|
+
write {|file| file.printf content.to_s}
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
# Calls open with 'w' argument and makes sure that the
|
90
|
+
# parent directory of the file exists
|
91
|
+
def write(&block)
|
92
|
+
parent.mkdirs
|
93
|
+
open('w', &block)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Calls open with 'a' argument and make sure that the
|
97
|
+
# parent directory of the file exists
|
98
|
+
def append(&block)
|
99
|
+
parent.mkdirs
|
100
|
+
open('a', &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Calls open with 'wb' argument, sets the io to binmode
|
104
|
+
# and make sure that the parent directory of the file exists
|
105
|
+
def write_binary(&block)
|
106
|
+
parent.mkdirs
|
107
|
+
if (block_given?)
|
108
|
+
open('wb') do |io|
|
109
|
+
io.binmode
|
110
|
+
yield io
|
111
|
+
end
|
112
|
+
else
|
113
|
+
io = open('wb')
|
114
|
+
io.binmode
|
115
|
+
io
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
=begin rdoc
|
120
|
+
Loading the file full total. This is used generally for loading
|
121
|
+
an ascii file content. It does not work with binary file on
|
122
|
+
windows system because it does no put the system on binary mode
|
123
|
+
=end
|
124
|
+
def load
|
125
|
+
content = nil
|
126
|
+
size = stat.size
|
127
|
+
read do |io|
|
128
|
+
content = io.read
|
129
|
+
end
|
130
|
+
return content
|
131
|
+
end
|
132
|
+
|
133
|
+
# calls open with 'r' as argument.
|
134
|
+
def read(&block)
|
135
|
+
open('r', &block)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Calls open with 'r' as argument and sets the io to binary mode
|
139
|
+
def read_binary
|
140
|
+
if block_given?
|
141
|
+
open('r') do |io|
|
142
|
+
io.binmode
|
143
|
+
yield io
|
144
|
+
end
|
145
|
+
else
|
146
|
+
io = open('r')
|
147
|
+
io.binmode
|
148
|
+
io
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# reads the file and calls back for each line
|
153
|
+
def foreach()
|
154
|
+
open('r') do |file|
|
155
|
+
file.each {|line| yield line}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# opens the file with the g iven arguments
|
160
|
+
def open(*args)
|
161
|
+
result = f = factory.system.io(@path, *args)
|
162
|
+
if block_given?
|
163
|
+
begin
|
164
|
+
result = yield f
|
165
|
+
ensure
|
166
|
+
f.close unless f.closed?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
result
|
170
|
+
end
|
171
|
+
|
172
|
+
# deletes the file
|
173
|
+
def delete
|
174
|
+
factory.system.delete_file(@path)
|
175
|
+
end
|
176
|
+
|
177
|
+
# reads this file as an archive and extracts the content
|
178
|
+
# to the target directory. If the target directory
|
179
|
+
# is missing, it will create a directory in the same
|
180
|
+
# directory as this file with the same basename
|
181
|
+
def extract(directory = nil)
|
182
|
+
require 'rubygems/package'
|
183
|
+
directory = parent.dir(basename) unless directory
|
184
|
+
read_binary do |io|
|
185
|
+
reader = Gem::Package::TarReader.new(io)
|
186
|
+
reader.each do |entry|
|
187
|
+
full_name = entry.full_name
|
188
|
+
if (entry.file?)
|
189
|
+
directory.file(full_name).write_binary do |output|
|
190
|
+
CottaFile.copy_io(entry, output)
|
191
|
+
end
|
192
|
+
elsif (entry.directory?)
|
193
|
+
directory.dir(full_name).mkdirs
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
directory
|
198
|
+
end
|
199
|
+
|
200
|
+
# Create the zip file from current file
|
201
|
+
# When target is nil, the output will be the name of the current file appended with ".zip"
|
202
|
+
def zip(target = nil, level=nil, strategy=nil)
|
203
|
+
target = parent.file("#{name}.zip") unless target
|
204
|
+
read_binary do |read_io|
|
205
|
+
target.write_binary do |write_io|
|
206
|
+
gz = Zlib::GzipWriter.new(write_io, level, strategy)
|
207
|
+
CottaFile.copy_io(read_io, gz)
|
208
|
+
gz.close
|
209
|
+
end
|
210
|
+
end
|
211
|
+
target
|
212
|
+
end
|
213
|
+
|
214
|
+
# unzips the file
|
215
|
+
def unzip
|
216
|
+
name = basename
|
217
|
+
if (extname.length == 0)
|
218
|
+
name = "#{name}.unzip"
|
219
|
+
end
|
220
|
+
target = parent.file(name)
|
221
|
+
read_binary do |read_io|
|
222
|
+
target.write_binary do |write_io|
|
223
|
+
gz = Zlib::GzipReader.new(read_io)
|
224
|
+
CottaFile.copy_io(gz, write_io)
|
225
|
+
gz.close
|
226
|
+
end
|
227
|
+
end
|
228
|
+
target
|
229
|
+
end
|
230
|
+
|
231
|
+
# copy from input handle to output handle
|
232
|
+
def self::copy_io(input, output)
|
233
|
+
# output.write input.read
|
234
|
+
while (content = input.read(1024)) do
|
235
|
+
output.write content
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def ==(other)
|
240
|
+
return false unless other.kind_of? CottaFile
|
241
|
+
return factory.system == other.factory.system && @path == other.path
|
242
|
+
end
|
243
|
+
|
244
|
+
def to_s
|
245
|
+
@path.to_s
|
246
|
+
end
|
247
|
+
|
248
|
+
def inspect
|
249
|
+
return "#{self.class}:#{self.object_id}-#{factory.system.inspect}-#@path"
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
end
|