text_tunnel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +63 -0
- data/Guardfile +24 -0
- data/Rakefile +1 -0
- data/bin/text_tunnel +49 -0
- data/bin/text_tunneld +63 -0
- data/lib/text_tunnel/client.rb +56 -0
- data/lib/text_tunnel/server.rb +44 -0
- data/lib/text_tunnel/version.rb +3 -0
- data/lib/text_tunnel/watched_file.rb +47 -0
- data/lib/text_tunnel/watched_file_repository.rb +20 -0
- data/lib/text_tunnel.rb +5 -0
- data/readme.md +49 -0
- data/test/test_helper.rb +5 -0
- data/test/text_tunnel/server_test.rb +49 -0
- data/test/text_tunnel/watched_file_repository_test.rb +33 -0
- data/test/text_tunnel/watched_file_test.rb +60 -0
- data/text_tunnel.gemspec +29 -0
- metadata +215 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
text_tunnel (0.1.0)
|
5
|
+
rest-client (~> 1.6.7)
|
6
|
+
sinatra (~> 1.3.2)
|
7
|
+
thin (~> 1.3.1)
|
8
|
+
trollop (~> 1.16.2)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: https://rubygems.org/
|
12
|
+
specs:
|
13
|
+
ansi (1.4.3)
|
14
|
+
daemons (1.1.8)
|
15
|
+
eventmachine (0.12.10)
|
16
|
+
ffi (1.0.11)
|
17
|
+
guard (1.2.3)
|
18
|
+
listen (>= 0.4.2)
|
19
|
+
thor (>= 0.14.6)
|
20
|
+
guard-minitest (0.5.0)
|
21
|
+
guard (>= 0.4)
|
22
|
+
listen (0.4.7)
|
23
|
+
rb-fchange (~> 0.0.5)
|
24
|
+
rb-fsevent (~> 0.9.1)
|
25
|
+
rb-inotify (~> 0.8.8)
|
26
|
+
mime-types (1.19)
|
27
|
+
minitest (3.2.0)
|
28
|
+
rack (1.4.1)
|
29
|
+
rack-protection (1.2.0)
|
30
|
+
rack
|
31
|
+
rack-test (0.6.1)
|
32
|
+
rack (>= 1.0)
|
33
|
+
rb-fchange (0.0.5)
|
34
|
+
ffi
|
35
|
+
rb-fsevent (0.9.1)
|
36
|
+
rb-inotify (0.8.8)
|
37
|
+
ffi (>= 0.5.0)
|
38
|
+
rest-client (1.6.7)
|
39
|
+
mime-types (>= 1.16)
|
40
|
+
sinatra (1.3.2)
|
41
|
+
rack (~> 1.3, >= 1.3.6)
|
42
|
+
rack-protection (~> 1.2)
|
43
|
+
tilt (~> 1.3, >= 1.3.3)
|
44
|
+
thin (1.3.1)
|
45
|
+
daemons (>= 1.0.9)
|
46
|
+
eventmachine (>= 0.12.6)
|
47
|
+
rack (>= 1.0.0)
|
48
|
+
thor (0.15.4)
|
49
|
+
tilt (1.3.3)
|
50
|
+
trollop (1.16.2)
|
51
|
+
turn (0.9.6)
|
52
|
+
ansi
|
53
|
+
|
54
|
+
PLATFORMS
|
55
|
+
ruby
|
56
|
+
|
57
|
+
DEPENDENCIES
|
58
|
+
guard (~> 1.2.3)
|
59
|
+
guard-minitest (~> 0.5.0)
|
60
|
+
minitest (~> 3.2.0)
|
61
|
+
rack-test (~> 0.6.1)
|
62
|
+
text_tunnel!
|
63
|
+
turn (~> 0.9.6)
|
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'minitest' do
|
5
|
+
# with Minitest::Unit
|
6
|
+
watch(%r|^test/(.*)\/?(.*)_test\.rb|)
|
7
|
+
watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
|
8
|
+
watch(%r|^test/test_helper\.rb|) { "test" }
|
9
|
+
|
10
|
+
# with Minitest::Spec
|
11
|
+
# watch(%r|^spec/(.*)_spec\.rb|)
|
12
|
+
# watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
13
|
+
# watch(%r|^spec/spec_helper\.rb|) { "spec" }
|
14
|
+
|
15
|
+
# Rails 3.2
|
16
|
+
# watch(%r|^app/controllers/(.*)\.rb|) { |m| "test/controllers/#{m[1]}_test.rb" }
|
17
|
+
# watch(%r|^app/helpers/(.*)\.rb|) { |m| "test/helpers/#{m[1]}_test.rb" }
|
18
|
+
# watch(%r|^app/models/(.*)\.rb|) { |m| "test/unit/#{m[1]}_test.rb" }
|
19
|
+
|
20
|
+
# Rails
|
21
|
+
# watch(%r|^app/controllers/(.*)\.rb|) { |m| "test/functional/#{m[1]}_test.rb" }
|
22
|
+
# watch(%r|^app/helpers/(.*)\.rb|) { |m| "test/helpers/#{m[1]}_test.rb" }
|
23
|
+
# watch(%r|^app/models/(.*)\.rb|) { |m| "test/unit/#{m[1]}_test.rb" }
|
24
|
+
end
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/text_tunnel
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
gem 'trollop', '~> 1.16.2'
|
4
|
+
require "trollop"
|
5
|
+
|
6
|
+
require "text_tunnel/version"
|
7
|
+
|
8
|
+
config = Trollop.options do
|
9
|
+
version "text_tunnel #{TextTunnel::VERSION}"
|
10
|
+
banner <<-EOS
|
11
|
+
Usage:
|
12
|
+
text_tunnel [options] <filename>
|
13
|
+
where [options] are:
|
14
|
+
EOS
|
15
|
+
|
16
|
+
opt :port, "Port", :type => :int, :default => 1777
|
17
|
+
end
|
18
|
+
|
19
|
+
Trollop.die "text_tunnel requires exactly one filename argument" unless ARGV.size == 1
|
20
|
+
file_path = ARGV[0]
|
21
|
+
|
22
|
+
require "text_tunnel/client"
|
23
|
+
begin
|
24
|
+
client = Client.new(config[:port], file_path)
|
25
|
+
puts "Editing #{file_path} via text_tunnel, CRTL+C to stop"
|
26
|
+
|
27
|
+
loop do
|
28
|
+
if client.poll
|
29
|
+
puts "Wrote #{file_path} at #{Time.now}"
|
30
|
+
end
|
31
|
+
|
32
|
+
sleep 1
|
33
|
+
end
|
34
|
+
rescue Errno::ECONNREFUSED => e
|
35
|
+
puts "ERROR: Unable to connect to text_tunneld web server."
|
36
|
+
rescue Errno::EISDIR
|
37
|
+
puts "ERROR: #{file_path} is a directory."
|
38
|
+
rescue Errno::EACCES
|
39
|
+
puts "ERROR: Access denied - #{file_path}"
|
40
|
+
rescue RestClient::Exception, UnexpectedResponseError => e
|
41
|
+
puts "ERROR: Connected to a web server, but received an unexpected response."
|
42
|
+
puts "HTTP Status Code: #{e.http_code}"
|
43
|
+
puts "HTTP Body:"
|
44
|
+
puts e.http_body
|
45
|
+
rescue Interrupt
|
46
|
+
puts "Exiting..."
|
47
|
+
ensure
|
48
|
+
client.cleanup if client
|
49
|
+
end
|
data/bin/text_tunneld
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
gem 'trollop', '~> 1.16.2'
|
4
|
+
require "trollop"
|
5
|
+
|
6
|
+
require "text_tunnel/version"
|
7
|
+
|
8
|
+
config = Trollop.options do
|
9
|
+
version "text_tunneld #{TextTunnel::VERSION}"
|
10
|
+
|
11
|
+
opt :editor, "Editor command e.g. /usr/local/bin/subl (otherwise will use ENV[\"EDITOR\"])", :type => :string
|
12
|
+
opt :daemon, "Run as daemon in background"
|
13
|
+
opt :port, "Port", :type => :int, :default => 1777
|
14
|
+
opt :log, "Log file location", :type => :string
|
15
|
+
opt :pid, "PID file location", :type => :string
|
16
|
+
end
|
17
|
+
|
18
|
+
editor = config[:editor] || ENV["EDITOR"]
|
19
|
+
Trollop.die "No --editor argument or EDITOR environment variable" unless editor
|
20
|
+
|
21
|
+
require "text_tunnel/watched_file"
|
22
|
+
require "text_tunnel/watched_file_repository"
|
23
|
+
require "text_tunnel/server"
|
24
|
+
|
25
|
+
File.umask(0077)
|
26
|
+
|
27
|
+
Server.configure do |s|
|
28
|
+
s.set :environment, "production"
|
29
|
+
s.set :bind, "127.0.0.1"
|
30
|
+
s.set :port, config[:port]
|
31
|
+
|
32
|
+
# Nesting callback because Sinatra evaluates the first automatically, so to
|
33
|
+
# get a callback usable by Server it has to be wrapped.
|
34
|
+
s.set :editor_spawner do
|
35
|
+
Proc.new do |local_path|
|
36
|
+
# Ordinarily, string concatenation should not be used for the command
|
37
|
+
# string to spawn. However, if editor and local path are passed as
|
38
|
+
# separate variables spawn will fail if the editor command has options
|
39
|
+
# as it will treat the options as part of the file path.
|
40
|
+
pid = spawn "#{editor} #{local_path}"
|
41
|
+
Process.detach(pid)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
s.set :watched_files, WatchedFileRepository.new
|
46
|
+
|
47
|
+
s.enable :logging
|
48
|
+
end
|
49
|
+
|
50
|
+
if config[:daemon]
|
51
|
+
Process.daemon(true)
|
52
|
+
end
|
53
|
+
|
54
|
+
if config[:log]
|
55
|
+
STDERR.reopen(open(config[:log], "w+"))
|
56
|
+
end
|
57
|
+
|
58
|
+
if config[:pid]
|
59
|
+
File.write(config[:pid], Process.pid.to_s)
|
60
|
+
at_exit { File.delete(config[:pid]) }
|
61
|
+
end
|
62
|
+
|
63
|
+
Server.run!
|
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
gem 'rest-client', '~> 1.6.7'
|
4
|
+
require "rest_client"
|
5
|
+
|
6
|
+
# Interface is compatible with RestClient exception
|
7
|
+
class UnexpectedResponseError < StandardError
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
def initialize(response)
|
11
|
+
@response = response
|
12
|
+
end
|
13
|
+
|
14
|
+
def http_code
|
15
|
+
response.code.to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def http_body
|
19
|
+
response.body
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Client
|
24
|
+
# Establishes initial connection to text_tunneld server
|
25
|
+
def initialize(port, file_path)
|
26
|
+
@file_path = file_path
|
27
|
+
|
28
|
+
file_name = File.basename(@file_path)
|
29
|
+
file_data = File.exist?(@file_path) ? File.read(@file_path) : ""
|
30
|
+
|
31
|
+
response = RestClient.post "http://localhost:#{port}/files",
|
32
|
+
:name => file_name,
|
33
|
+
:data => file_data
|
34
|
+
raise UnexpectedResponseError.new(response) unless response.code == 201
|
35
|
+
@location = response.headers[:location]
|
36
|
+
@etag = response.headers[:etag]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a truthy value if a change was made
|
40
|
+
def poll
|
41
|
+
response = RestClient.get(@location, :if_none_match => @etag)
|
42
|
+
raise UnexpectedResponseError.new(response) unless response.code == 200
|
43
|
+
|
44
|
+
@etag = response.headers[:etag]
|
45
|
+
|
46
|
+
File.write(@file_path, response.body)
|
47
|
+
rescue RestClient::NotModified
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def cleanup
|
52
|
+
# This call can fail, especially if text_tunnel is terminating because of
|
53
|
+
# a previous error. So swallow any errors.
|
54
|
+
RestClient.delete(@location) rescue nil
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
gem "sinatra", "~> 1.3.2"
|
2
|
+
require "sinatra/base"
|
3
|
+
|
4
|
+
class Server < Sinatra::Base
|
5
|
+
post '/files' do
|
6
|
+
watched_file = watched_files.create(params[:name], params[:data])
|
7
|
+
spawn_editor(watched_file.local_path)
|
8
|
+
|
9
|
+
logger.info "#{watched_file.id} - new - #{params[:name]} (#{watched_file.data.size} bytes)"
|
10
|
+
|
11
|
+
status 201
|
12
|
+
etag watched_file.hash
|
13
|
+
headers "Location" => url("/files/#{watched_file.id}")
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
get "/files/:id" do
|
18
|
+
logger.debug "#{params[:id]} - poll"
|
19
|
+
|
20
|
+
watched_file = watched_files.find(params[:id])
|
21
|
+
watched_file.poll
|
22
|
+
etag watched_file.hash
|
23
|
+
body watched_file.data
|
24
|
+
|
25
|
+
logger.info "#{params[:id]} - sent - #{watched_file.data.size} bytes"
|
26
|
+
end
|
27
|
+
|
28
|
+
delete "/files/:id" do
|
29
|
+
watched_file = watched_files.find(params[:id])
|
30
|
+
watched_files.remove(watched_file)
|
31
|
+
|
32
|
+
logger.info "#{params[:id]} - deleted"
|
33
|
+
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def watched_files
|
38
|
+
settings.watched_files
|
39
|
+
end
|
40
|
+
|
41
|
+
def spawn_editor(local_path)
|
42
|
+
settings.editor_spawner.call(local_path)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "tmpdir"
|
2
|
+
require "fileutils"
|
3
|
+
require "securerandom"
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
class WatchedFile
|
7
|
+
attr_reader :id
|
8
|
+
attr_reader :local_path
|
9
|
+
attr_reader :data
|
10
|
+
attr_reader :hash
|
11
|
+
|
12
|
+
def initialize(name, data)
|
13
|
+
@name = sanitize_name(name)
|
14
|
+
@data = data
|
15
|
+
@id = SecureRandom.hex
|
16
|
+
@local_dir = "#{Dir.tmpdir}/text-tunnel/#{id}"
|
17
|
+
@local_path = "#{@local_dir}/#{@name}"
|
18
|
+
|
19
|
+
write_temp_file
|
20
|
+
end
|
21
|
+
|
22
|
+
def poll
|
23
|
+
old_mtime = @mtime
|
24
|
+
@mtime = File.mtime(local_path)
|
25
|
+
if @mtime != old_mtime
|
26
|
+
@mtime = old_mtime
|
27
|
+
@data = File.read(local_path)
|
28
|
+
hash_data
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def write_temp_file
|
34
|
+
FileUtils.mkdir_p @local_dir
|
35
|
+
File.write(@local_path, @data)
|
36
|
+
@mtime = File.mtime(local_path)
|
37
|
+
hash_data
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash_data
|
41
|
+
@hash = Digest::SHA1.hexdigest(@data)
|
42
|
+
end
|
43
|
+
|
44
|
+
def sanitize_name(name)
|
45
|
+
name.gsub(/[^a-zA-Z0-9\-_. ]/, "").strip
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "text_tunnel/watched_file"
|
2
|
+
|
3
|
+
class WatchedFileRepository
|
4
|
+
def initialize
|
5
|
+
@watched_files = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def create(name, data)
|
9
|
+
watched_file = WatchedFile.new(name, data)
|
10
|
+
@watched_files[watched_file.id] = watched_file
|
11
|
+
end
|
12
|
+
|
13
|
+
def remove(watched_file)
|
14
|
+
@watched_files.delete(watched_file.id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find(id)
|
18
|
+
@watched_files.fetch(id)
|
19
|
+
end
|
20
|
+
end
|
data/lib/text_tunnel.rb
ADDED
data/readme.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Text Tunnel
|
2
|
+
|
3
|
+
Text Tunnel is a tool to edit remote files with a local editor.
|
4
|
+
|
5
|
+
## How It Works
|
6
|
+
|
7
|
+
The text_tunneld server runs on the local host. Using SSH, a reverse port
|
8
|
+
forward is established on the remote host connecting back to the server. The
|
9
|
+
text_tunnel client runs on the remote machine and is used as the editor
|
10
|
+
binary. When the client is told to edit a file it sends the file to the
|
11
|
+
server, and the server loads it in the local editor. The client polls the
|
12
|
+
server for changes and downloads them whenever they occur.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
The text_tunnel gem needs to be installed on both the local and remote hosts.
|
17
|
+
|
18
|
+
gem install text_tunnel
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Start the server on the local machine. If you don't have an EDITOR environment
|
23
|
+
variable set you will need to include an --editor option.
|
24
|
+
|
25
|
+
text_tunneld -e /usr/local/bin/subl
|
26
|
+
|
27
|
+
Connect to the remote host with SSH and specify a reverse port forward (the
|
28
|
+
default port for Text Tunnel is 1777).
|
29
|
+
|
30
|
+
ssh -R 1777:localhost:1777 remote-host
|
31
|
+
|
32
|
+
On the remote machine use text_tunnel as your editor.
|
33
|
+
|
34
|
+
text_tunnel /path/to/file
|
35
|
+
|
36
|
+
The file should open on your local machine. Do your edits and save your file.
|
37
|
+
It will automatically be transferred to the remote host. When you are done hit
|
38
|
+
Crtl+C on the remote host to terminate Text Tunnel.
|
39
|
+
|
40
|
+
## Shortcuts
|
41
|
+
|
42
|
+
The reverse port forward can be configured in ~/.ssh/config to avoid having to
|
43
|
+
retype the reverse port forward on the command line every time.
|
44
|
+
|
45
|
+
Consider setting text_tunnel as your EDITOR environment variable on your
|
46
|
+
remote hosts. This will let you use a local text editor for git commit
|
47
|
+
messages, crontabs, etc.
|
48
|
+
|
49
|
+
text_tunneld supports a background daemon mode. Run text_tunneld -h for full options.
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
require "rack/test"
|
3
|
+
require "text_tunnel/watched_file"
|
4
|
+
require "text_tunnel/watched_file_repository"
|
5
|
+
require "text_tunnel/server"
|
6
|
+
|
7
|
+
Server.configure do |s|
|
8
|
+
s.set :editor_spawner do
|
9
|
+
Proc.new {}
|
10
|
+
end
|
11
|
+
s.set :watched_files, WatchedFileRepository.new
|
12
|
+
end
|
13
|
+
|
14
|
+
class ServerTest < MiniTest::Unit::TestCase
|
15
|
+
include Rack::Test::Methods
|
16
|
+
|
17
|
+
def app
|
18
|
+
Server
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@watched_files = WatchedFileRepository.new
|
23
|
+
Server.configure do |s|
|
24
|
+
s.set :watched_files, @watched_files
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_complete_editing_session
|
29
|
+
post "/files", "name" => "foo", :data => "bar"
|
30
|
+
location = last_response["Location"]
|
31
|
+
etag = last_response["Etag"]
|
32
|
+
|
33
|
+
header("If-None-Match", etag)
|
34
|
+
get location
|
35
|
+
assert_equal 304, last_response.status
|
36
|
+
|
37
|
+
watched_file_id = location[/\w+\Z/]
|
38
|
+
local_file = @watched_files.find(watched_file_id).local_path
|
39
|
+
File.write(local_file, "new content")
|
40
|
+
|
41
|
+
get location
|
42
|
+
assert_equal 200, last_response.status
|
43
|
+
assert_equal "new content", last_response.body
|
44
|
+
|
45
|
+
delete location
|
46
|
+
assert_equal 200, last_response.status
|
47
|
+
assert_raises(KeyError) { @watched_files.find(watched_file_id) }
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
require "text_tunnel/watched_file_repository"
|
3
|
+
|
4
|
+
class WatchedFileRepositoryTest < MiniTest::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
@repo = WatchedFileRepository.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_create_returns_a_WatchedFile
|
10
|
+
watched_file = @repo.create "foo", "bar"
|
11
|
+
assert_kind_of WatchedFile, watched_file
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_create_stores_created_WatchedFile
|
15
|
+
watched_file = @repo.create "foo", "bar"
|
16
|
+
assert_equal watched_file, @repo.find(watched_file.id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_remove_makes_watched_file_no_longer_findable
|
20
|
+
watched_file = @repo.create "foo", "bar"
|
21
|
+
@repo.remove(watched_file)
|
22
|
+
assert_raises(KeyError) { @repo.find(watched_file.id) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_find_existing_WatchedFile
|
26
|
+
watched_file = @repo.create "foo", "bar"
|
27
|
+
assert_equal watched_file, @repo.find(watched_file.id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_find_raises_KeyError_on_missing_id
|
31
|
+
assert_raises(KeyError) { @repo.find("missing") }
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative "../test_helper"
|
2
|
+
require "text_tunnel/watched_file"
|
3
|
+
|
4
|
+
class WatchedFileTest < MiniTest::Unit::TestCase
|
5
|
+
def test_id_is_constant
|
6
|
+
wf = WatchedFile.new("foo", "bar")
|
7
|
+
assert_equal wf.id, wf.id
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_id_is_different_for_each_object
|
11
|
+
refute_equal WatchedFile.new("foo", "bar").id, WatchedFile.new("foo", "bar").id
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_local_path_points_to_file_with_data
|
15
|
+
wf = WatchedFile.new("foo", "bar")
|
16
|
+
assert_equal "bar", File.read(wf.local_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_poll_updates_data_and_hash_when_file_changes
|
20
|
+
wf = WatchedFile.new("foo", "bar")
|
21
|
+
old_hash, old_data = wf.hash, wf.data
|
22
|
+
|
23
|
+
# Without the sleep the original write and the following are so close
|
24
|
+
# together that the mtime is the same. I suppose this could be fixed by
|
25
|
+
# using guard-listen or EventMachine::FileWatcher, but in real usage it
|
26
|
+
# shouldn't be an issue. I prefer to keep the brain-dead simple polling
|
27
|
+
# instead of complicating this by requiring eventing or threading.
|
28
|
+
sleep 0.1
|
29
|
+
|
30
|
+
File.write(wf.local_path, "quz")
|
31
|
+
wf.poll
|
32
|
+
|
33
|
+
refute_equal old_hash, wf.hash
|
34
|
+
refute_equal old_data, wf.data
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_sanitize_name_replaces_bad_characters
|
38
|
+
dirty_name = "/\\\"'asdf#!@$%^&*(){}}<>?'"
|
39
|
+
sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
|
40
|
+
assert_equal "asdf", sanitized_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_sanitize_name_does_not_replace_periods
|
44
|
+
dirty_name = "foo.rb"
|
45
|
+
sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
|
46
|
+
assert_equal "foo.rb", sanitized_name
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_sanitize_name_leaves_spaced_in_middle
|
50
|
+
dirty_name = "foo bar.rb"
|
51
|
+
sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
|
52
|
+
assert_equal "foo bar.rb", sanitized_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_sanitize_name_trims_spaces_on_edges
|
56
|
+
dirty_name = " foo bar.rb "
|
57
|
+
sanitized_name = WatchedFile.new("foo", "bar").instance_eval { sanitize_name(dirty_name) }
|
58
|
+
assert_equal "foo bar.rb", sanitized_name
|
59
|
+
end
|
60
|
+
end
|
data/text_tunnel.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/text_tunnel/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jack Christensen"]
|
6
|
+
gem.email = ["jack@jackchristensen.com"]
|
7
|
+
gem.description = %q{Use your local text editor to edit files on remote servers.}
|
8
|
+
gem.summary = %q{Contains client and server that enables editing remote files with a local text editor.}
|
9
|
+
gem.homepage = "https://github.com/JackC/text_tunnel"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "text_tunnel"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = TextTunnel::VERSION
|
17
|
+
|
18
|
+
|
19
|
+
gem.add_dependency 'rest-client', '~> 1.6.7'
|
20
|
+
gem.add_dependency 'sinatra', "~> 1.3.2"
|
21
|
+
gem.add_dependency 'trollop', '~> 1.16.2'
|
22
|
+
gem.add_dependency 'thin', '~> 1.3.1'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'minitest', "~> 3.2.0"
|
25
|
+
gem.add_development_dependency 'turn', "~> 0.9.6"
|
26
|
+
gem.add_development_dependency 'guard', "~> 1.2.3"
|
27
|
+
gem.add_development_dependency 'guard-minitest', "~> 0.5.0"
|
28
|
+
gem.add_development_dependency 'rack-test', '~> 0.6.1'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: text_tunnel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jack Christensen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-21 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rest-client
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.6.7
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.6.7
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sinatra
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.3.2
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.3.2
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: trollop
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.16.2
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.16.2
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: thin
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.3.1
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.3.1
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: minitest
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 3.2.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 3.2.0
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: turn
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 0.9.6
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 0.9.6
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: guard
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.2.3
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.2.3
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: guard-minitest
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ~>
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: 0.5.0
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ~>
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: 0.5.0
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: rack-test
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ~>
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: 0.6.1
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ~>
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: 0.6.1
|
158
|
+
description: Use your local text editor to edit files on remote servers.
|
159
|
+
email:
|
160
|
+
- jack@jackchristensen.com
|
161
|
+
executables:
|
162
|
+
- text_tunnel
|
163
|
+
- text_tunneld
|
164
|
+
extensions: []
|
165
|
+
extra_rdoc_files: []
|
166
|
+
files:
|
167
|
+
- .gitignore
|
168
|
+
- Gemfile
|
169
|
+
- Gemfile.lock
|
170
|
+
- Guardfile
|
171
|
+
- Rakefile
|
172
|
+
- bin/text_tunnel
|
173
|
+
- bin/text_tunneld
|
174
|
+
- lib/text_tunnel.rb
|
175
|
+
- lib/text_tunnel/client.rb
|
176
|
+
- lib/text_tunnel/server.rb
|
177
|
+
- lib/text_tunnel/version.rb
|
178
|
+
- lib/text_tunnel/watched_file.rb
|
179
|
+
- lib/text_tunnel/watched_file_repository.rb
|
180
|
+
- readme.md
|
181
|
+
- test/test_helper.rb
|
182
|
+
- test/text_tunnel/server_test.rb
|
183
|
+
- test/text_tunnel/watched_file_repository_test.rb
|
184
|
+
- test/text_tunnel/watched_file_test.rb
|
185
|
+
- text_tunnel.gemspec
|
186
|
+
homepage: https://github.com/JackC/text_tunnel
|
187
|
+
licenses: []
|
188
|
+
post_install_message:
|
189
|
+
rdoc_options: []
|
190
|
+
require_paths:
|
191
|
+
- lib
|
192
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
193
|
+
none: false
|
194
|
+
requirements:
|
195
|
+
- - ! '>='
|
196
|
+
- !ruby/object:Gem::Version
|
197
|
+
version: '0'
|
198
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
199
|
+
none: false
|
200
|
+
requirements:
|
201
|
+
- - ! '>='
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '0'
|
204
|
+
requirements: []
|
205
|
+
rubyforge_project:
|
206
|
+
rubygems_version: 1.8.23
|
207
|
+
signing_key:
|
208
|
+
specification_version: 3
|
209
|
+
summary: Contains client and server that enables editing remote files with a local
|
210
|
+
text editor.
|
211
|
+
test_files:
|
212
|
+
- test/test_helper.rb
|
213
|
+
- test/text_tunnel/server_test.rb
|
214
|
+
- test/text_tunnel/watched_file_repository_test.rb
|
215
|
+
- test/text_tunnel/watched_file_test.rb
|