rush 0.1

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.
@@ -0,0 +1,141 @@
1
+ # Rush::Entry is the base class for Rush::File and Rush::Dir. One or more of
2
+ # these is instantiated whenever you use square brackets to access the
3
+ # filesystem on a box, as well as any other operation that returns an entry or
4
+ # list of entries.
5
+ class Rush::Entry
6
+ attr_reader :box, :name, :path
7
+
8
+ # Initialize with full path to the file or dir, and the box it resides on.
9
+ def initialize(full_path, box=nil)
10
+ full_path = ::File.expand_path(full_path, '/')
11
+ @path = ::File.dirname(full_path)
12
+ @name = ::File.basename(full_path)
13
+ @box = box || Rush::Box.new('localhost')
14
+ end
15
+
16
+ # The factory checks to see if the full path has a trailing slash for
17
+ # creating a Rush::Dir rather than the default Rush::File.
18
+ def self.factory(full_path, box=nil)
19
+ if full_path.tail(1) == '/'
20
+ Rush::Dir.new(full_path, box)
21
+ else
22
+ Rush::File.new(full_path, box)
23
+ end
24
+ end
25
+
26
+ def to_s # :nodoc:
27
+ if box.host == 'localhost'
28
+ "#{full_path}"
29
+ else
30
+ inspect
31
+ end
32
+ end
33
+
34
+ def inspect # :nodoc:
35
+ "#{box}:#{full_path}"
36
+ end
37
+
38
+ def connection
39
+ box ? box.connection : Rush::Connection::Local.new
40
+ end
41
+
42
+ # The parent dir. For example, box['/etc/hosts'].parent == box['etc/']
43
+ def parent
44
+ @parent ||= Rush::Dir.new(@path)
45
+ end
46
+
47
+ def full_path
48
+ "#{@path}/#{@name}"
49
+ end
50
+
51
+ # Timestamp of entry creation.
52
+ def created_at
53
+ stat[:ctime]
54
+ end
55
+
56
+ # Timestamp that entry was last modified on.
57
+ def last_modified
58
+ stat[:mtime]
59
+ end
60
+
61
+ # Timestamp that entry was last accessed on.
62
+ def last_accessed
63
+ stat[:atime]
64
+ end
65
+
66
+ # Attempts to rename, copy, or otherwise place an entry into a dir that already contains an entry by that name will fail with this exception.
67
+ class NameAlreadyExists < Exception; end
68
+
69
+ # Do not use rename or duplicate with a slash; use copy_to or move_to instead.
70
+ class NameCannotContainSlash < Exception; end
71
+
72
+ # You cannot move or copy entries to a path that is not a dir.
73
+ class NotADir < Exception; end
74
+
75
+ # Rename an entry to another name within the same dir. The object's name
76
+ # will be updated to match the change on the filesystem.
77
+ def rename(new_name)
78
+ connection.rename(@path, @name, new_name)
79
+ @name = new_name
80
+ end
81
+
82
+ # Rename an entry to another name within the same dir. The existing object
83
+ # will not be affected, but a new object representing the newly-created
84
+ # entry will be returned.
85
+ def duplicate(new_name)
86
+ raise NameCannotContainSlash if new_name.match(/\//)
87
+ new_full_path = "#{@path}/#{new_name}"
88
+ connection.copy(full_path, new_full_path)
89
+ self.class.new(new_full_path, box)
90
+ end
91
+
92
+ # Copy the entry to another dir. Returns an object representing the new
93
+ # copy.
94
+ def copy_to(dir)
95
+ raise NotADir unless dir.class == Rush::Dir
96
+
97
+ if box == dir.box
98
+ connection.copy(full_path, dir.full_path)
99
+ else
100
+ archive = connection.read_archive(full_path)
101
+ dir.box.connection.write_archive(archive, dir.full_path)
102
+ end
103
+
104
+ new_full_path = "#{dir.full_path}#{name}"
105
+ self.class.new(new_full_path, dir.box)
106
+ end
107
+
108
+ # Move the entry to another dir. The object will be updated to show its new
109
+ # location.
110
+ def move_to(dir)
111
+ moved = copy_to(dir)
112
+ destroy
113
+ mimic(moved)
114
+ end
115
+
116
+ def mimic(from) # :nodoc:
117
+ @box = from.box
118
+ @path = from.path
119
+ @name = from.name
120
+ end
121
+
122
+ # Unix convention considers entries starting with a . to be hidden.
123
+ def hidden?
124
+ name.slice(0, 1) == '.'
125
+ end
126
+
127
+ # Destroy the entry. If it is a dir, everything inside it will also be destroyed.
128
+ def destroy
129
+ connection.destroy(full_path)
130
+ end
131
+
132
+ def ==(other) # :nodoc:
133
+ full_path == other.full_path and box == other.box
134
+ end
135
+
136
+ private
137
+
138
+ def stat
139
+ connection.stat(full_path)
140
+ end
141
+ end
@@ -0,0 +1,73 @@
1
+ # Files are a subclass of Rush::Entry. Most of the file-specific operations
2
+ # relate to manipulating the file's contents, like search and replace.
3
+ class Rush::File < Rush::Entry
4
+ def dir?
5
+ false
6
+ end
7
+
8
+ # Create a blank file.
9
+ def create
10
+ write('')
11
+ self
12
+ end
13
+
14
+ # Size in bytes on disk.
15
+ def size
16
+ stat[:size]
17
+ end
18
+
19
+ # Raw contents of the file. For non-text files, you probably want to avoid
20
+ # printing this on the screen.
21
+ def contents
22
+ connection.file_contents(full_path)
23
+ end
24
+
25
+ # Write to the file, overwriting whatever was already in it.
26
+ #
27
+ # Example: file.write "hello, world\n"
28
+ def write(new_contents)
29
+ connection.write_file(full_path, new_contents)
30
+ end
31
+
32
+ # Return an array of lines from the file, similar to stdlib's File#readlines.
33
+ def lines
34
+ contents.split("\n")
35
+ end
36
+
37
+ # Search the file's for a regular expression. Returns nil if no match, or
38
+ # each of the matching lines in its entirety.
39
+ #
40
+ # Example: box['/etc/hosts'].search(/localhost/) # -> [ "127.0.0.1 localhost\n", "::1 localhost\n" ]
41
+ def search(pattern)
42
+ matching_lines = lines.select { |line| line.match(pattern) }
43
+ matching_lines.size == 0 ? nil : matching_lines
44
+ end
45
+
46
+ # Search-and-replace file contents.
47
+ #
48
+ # Example: box['/etc/hosts'].replace_contents!(/localhost/, 'local.host')
49
+ def replace_contents!(pattern, replace_with)
50
+ write contents.gsub(pattern, replace_with)
51
+ end
52
+
53
+ # Return the file's contents, or if it doesn't exist, a blank string.
54
+ def contents_or_blank
55
+ contents rescue ""
56
+ end
57
+
58
+ # Count the number of lines in the file.
59
+ def line_count
60
+ lines.size
61
+ end
62
+
63
+ # Return an array of lines, or an empty array if the file does not exist.
64
+ def lines_or_empty
65
+ lines rescue []
66
+ end
67
+
68
+ include Rush::Commands
69
+
70
+ def entries
71
+ [ self ]
72
+ end
73
+ end
@@ -0,0 +1,18 @@
1
+ # Integer extensions for file and dir sizes (returned in bytes).
2
+ #
3
+ # Example:
4
+ #
5
+ # box['/assets/'].files_flattened.select { |f| f.size > 10.mb }
6
+ class Fixnum
7
+ def kb
8
+ self * 1024
9
+ end
10
+
11
+ def mb
12
+ kb * 1024
13
+ end
14
+
15
+ def gb
16
+ mb * 1024
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # Mixin for array and string for methods I wish they had.
2
+ module Rush::HeadTail
3
+ def head(n)
4
+ slice(0, n)
5
+ end
6
+
7
+ def tail(n)
8
+ n = [ n, length ].min
9
+ slice(-n, n)
10
+ end
11
+ end
@@ -0,0 +1,224 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ # Rush::Box uses a connection object to execute all rush commands. If the box
5
+ # is local, Rush::Connection::Local is created. The local connection is the
6
+ # heart of rush's internals. (Users of the rush shell or library need never
7
+ # access the connection object directly, so the docs herein are intended for
8
+ # developers wishing to modify rush.)
9
+ #
10
+ # The local connection has a series of methods which do the actual work of
11
+ # modifying files, getting process lists, and so on. RushServer creates a
12
+ # local connection to handle incoming requests; the translation from a raw hash
13
+ # of parameters to an executed method is handled by
14
+ # Rush::Connection::Local#receive.
15
+ class Rush::Connection::Local
16
+ # Write raw bytes to a file.
17
+ def write_file(full_path, contents)
18
+ ::File.open(full_path, 'w') do |f|
19
+ f.write contents
20
+ end
21
+ true
22
+ end
23
+
24
+ # Read raw bytes from a file.
25
+ def file_contents(full_path)
26
+ ::File.read(full_path)
27
+ end
28
+
29
+ # Destroy a file or dir.
30
+ def destroy(full_path)
31
+ raise "No." if full_path == '/'
32
+ FileUtils.rm_rf(full_path)
33
+ true
34
+ end
35
+
36
+ # Create a dir.
37
+ def create_dir(full_path)
38
+ FileUtils.mkdir_p(full_path)
39
+ true
40
+ end
41
+
42
+ class NameAlreadyExists < Exception; end
43
+ class NameCannotContainSlash < Exception; end
44
+ class NotADir < Exception; end
45
+
46
+ # Rename an entry within a dir.
47
+ def rename(path, name, new_name)
48
+ raise NameCannotContainSlash if new_name.match(/\//)
49
+ old_full_path = "#{path}/#{name}"
50
+ new_full_path = "#{path}/#{new_name}"
51
+ raise NameAlreadyExists if ::File.exists?(new_full_path)
52
+ FileUtils.mv(old_full_path, new_full_path)
53
+ true
54
+ end
55
+
56
+ # Copy ane entry from one path to another.
57
+ def copy(src, dst)
58
+ FileUtils.cp_r(src, dst)
59
+ true
60
+ end
61
+
62
+ # Create an in-memory archive (tgz) of a file or dir, which can be
63
+ # transmitted to another server for a copy or move. Note that archive
64
+ # operations have the dir name implicit in the archive.
65
+ def read_archive(full_path)
66
+ `cd #{::File.dirname(full_path)}; tar c #{::File.basename(full_path)}`
67
+ end
68
+
69
+ # Extract an in-memory archive to a dir.
70
+ def write_archive(archive, dir)
71
+ IO.popen("cd #{dir}; tar x", "w") do |p|
72
+ p.write archive
73
+ end
74
+ end
75
+
76
+ # Get an index of files from the given path with the glob. Could return
77
+ # nested values if the glob contains a doubleglob. The return value is an
78
+ # array of full paths, with directories listed first.
79
+ def index(base_path, glob)
80
+ glob = '*' if glob == '' or glob.nil?
81
+ dirs = []
82
+ files = []
83
+ ::Dir.chdir(base_path) do
84
+ ::Dir.glob(glob).each do |fname|
85
+ if ::File.directory?(fname)
86
+ dirs << fname + '/'
87
+ else
88
+ files << fname
89
+ end
90
+ end
91
+ end
92
+ dirs + files
93
+ end
94
+
95
+ # Fetch stats (size, ctime, etc) on an entry. Size will not be accurate for dirs.
96
+ def stat(full_path)
97
+ s = ::File.stat(full_path)
98
+ {
99
+ :size => s.size,
100
+ :ctime => s.ctime,
101
+ :atime => s.atime,
102
+ :mtime => s.mtime,
103
+ }
104
+ end
105
+
106
+ # Fetch the size of a dir, since a standard file stat does not include the
107
+ # size of the contents.
108
+ def size(full_path)
109
+ `du -sb #{full_path}`.match(/(\d+)/)[1].to_i
110
+ end
111
+
112
+ # Get the list of processes as an array of hashes.
113
+ def processes
114
+ if ::File.directory? "/proc"
115
+ linux_processes
116
+ else
117
+ os_x_processes
118
+ end
119
+ end
120
+
121
+ # Process list on Linux using /proc.
122
+ def linux_processes
123
+ list = []
124
+ ::Dir["/proc/*/stat"].select { |file| file =~ /\/proc\/\d+\// }.each do |file|
125
+ begin
126
+ list << read_proc_file(file)
127
+ rescue
128
+ # process died between the dir listing and accessing the file
129
+ end
130
+ end
131
+ list
132
+ end
133
+
134
+ # Read a single file in /proc and store the parsed values in a hash suitable
135
+ # for use in the Rush::Process#new.
136
+ def read_proc_file(file)
137
+ data = ::File.read(file).split(" ")
138
+ uid = ::File.stat(file).uid
139
+ pid = data[0]
140
+ command = data[1].match(/^\((.*)\)$/)[1]
141
+ cmdline = ::File.read("/proc/#{pid}/cmdline").gsub(/\0/, ' ')
142
+ utime = data[13].to_i
143
+ ktime = data[14].to_i
144
+ vss = data[22].to_i / 1024
145
+ rss = data[23].to_i * 4
146
+ time = utime + ktime
147
+
148
+ {
149
+ :pid => pid,
150
+ :uid => uid,
151
+ :command => command,
152
+ :cmdline => cmdline,
153
+ :mem => rss,
154
+ :cpu => time,
155
+ }
156
+ end
157
+
158
+ # Process list on OS X or other unixes without a /proc.
159
+ def os_x_processes
160
+ raw = os_x_raw_ps.split("\n").slice(1, 99999)
161
+ raw.map do |line|
162
+ parse_ps(line)
163
+ end
164
+ end
165
+
166
+ # ps command used to generate list of processes on non-/proc unixes.
167
+ def os_x_raw_ps
168
+ `COLUMNS=9999 ps ax -o "pid uid rss cpu command"`
169
+ end
170
+
171
+ # Parse a single line of the ps command and return the values in a hash
172
+ # suitable for use in the Rush::Process#new.
173
+ def parse_ps(line)
174
+ m = line.split(" ", 5)
175
+ params = {}
176
+ params[:pid] = m[0]
177
+ params[:uid] = m[1]
178
+ params[:rss] = m[2]
179
+ params[:cpu] = m[3]
180
+ params[:cmdline] = m[4]
181
+ params[:command] = params[:cmdline].split(" ").first
182
+ params
183
+ end
184
+
185
+ # Returns true if the specified pid is running.
186
+ def process_alive(pid)
187
+ `ps -p #{pid} | wc -l`.to_i >= 2
188
+ end
189
+
190
+ # Terminate a process, by pid.
191
+ def kill_process(pid)
192
+ ::Process.kill('TERM', pid.to_i)
193
+ end
194
+
195
+ ####################################
196
+
197
+ # Raised when the action passed in by RushServer is not known.
198
+ class UnknownAction < Exception; end
199
+
200
+ # RushServer uses this method to transform a hash (:action plus parameters
201
+ # specific to that action type) into a method call on the connection. The
202
+ # returned value must be text so that it can be transmitted across the wire
203
+ # as an HTTP response.
204
+ def receive(params)
205
+ case params[:action]
206
+ when 'write_file' then write_file(params[:full_path], params[:payload])
207
+ when 'file_contents' then file_contents(params[:full_path])
208
+ when 'destroy' then destroy(params[:full_path])
209
+ when 'create_dir' then create_dir(params[:full_path])
210
+ when 'rename' then rename(params[:path], params[:name], params[:new_name])
211
+ when 'copy' then copy(params[:src], params[:dst])
212
+ when 'read_archive' then read_archive(params[:full_path])
213
+ when 'write_archive' then write_archive(params[:payload], params[:dir])
214
+ when 'index' then index(params[:base_path], params[:glob]).join("\n") + "\n"
215
+ when 'stat' then YAML.dump(stat(params[:full_path]))
216
+ when 'size' then size(params[:full_path])
217
+ when 'processes' then YAML.dump(processes)
218
+ when 'process_alive' then process_alive(params[:pid]) ? '1' : '0'
219
+ when 'kill_process' then kill_process(params[:pid])
220
+ else
221
+ raise UnknownAction
222
+ end
223
+ end
224
+ end