text_tunnel 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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,3 @@
1
+ module TextTunnel
2
+ VERSION = "0.1.0"
3
+ 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
@@ -0,0 +1,5 @@
1
+ require "foo/version"
2
+
3
+ module TextTunnel
4
+ # Your code goes here...
5
+ end
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.
@@ -0,0 +1,5 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+ require 'turn/autorun'
4
+
5
+ ENV["RACK_ENV"] = "test"
@@ -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
@@ -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