text_tunnel 0.1.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/.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
|