sinatra-torrent 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sinatra-torrent"
8
+ gem.description = "An extension to Sinatra which will allow you to run a webseeded torrent tracker of files in the folder you specify."
9
+ gem.summary = "A sinatra extension to run webseeded torrent tracker"
10
+ gem.email = "jphastings@gmail.com"
11
+ gem.homepage = "http://github.com/jphastings/sinatra-torrent"
12
+ gem.authors = ["JP Hastings-Spital"]
13
+
14
+ gem.add_dependency('sinatra','>=1.1.2') # Required for send_file modifications
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
data/Readme.md ADDED
@@ -0,0 +1,37 @@
1
+ Sinatra-Torrent
2
+ ===============
3
+
4
+ There was [a /. article](http://ask.slashdot.org/story/10/10/04/0035231) about BitTorrent replacing standard downloads and I thought: Yes.
5
+
6
+ Ruby doesn't appear to like BitTorrent very much, most libraries are pretty old and I figured I'd spruce up my favourite jazz legend themed DSL with a library to make serving torrents *ridiculously* easy.
7
+
8
+ Usage
9
+ -----
10
+
11
+ require 'sinatra'
12
+ require 'sinatra/torrent'
13
+
14
+ "Woah, that's pretty simple!" I hear you say. Why yes, I think it is.
15
+
16
+ All files you put in the `downloads` directory at the root of your sinatra app will be downloadable at `/downloads/your_file.ext` and it's torrent will be dynamically generated (and cached) at `/torrents/your_file.ext.torrent`. You will have trouble with larger files as it currently hashes as part of the request first time round. I'm planning on pushing this out to workers at some point. Not yet sure how I'm going to do that…
17
+
18
+ **NB.** Files that take longer than 1s to hash will fail at the moment!
19
+
20
+ The extension is in it's early stages at the moment, so many of the settings aren't adhered to, and there are some issues with the webseeding… however it *does* work.
21
+
22
+ ### I want options!
23
+
24
+ There needs to be a database of torrents and peers, this is taken care of by a database adapter. Currently I've written (a breally basic) one for active record, so many databases are supported. I'm still finding my way around the Sinatra extensions api, so this is how you specify your own ActiveRecord settings:
25
+
26
+ require 'sinatra'
27
+ require 'sinatra/torrent/activerecord'
28
+ SinatraTorrentDatabase.settings = {
29
+ 'adapater' => 'sqlite3',
30
+ 'database' => 'torrents.db'
31
+ }
32
+ require 'sinatra/torrent'
33
+
34
+ Ummmm
35
+ -----
36
+
37
+ That's it for now. If you have any feed back - get in touch! You can use [twitter](http://twitter.com/jphastings), [github issues](http://github.com/jphastings/sinatra-torrent/issues) or any other medium you can think of.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.3
@@ -0,0 +1,161 @@
1
+ require 'timeout'
2
+ require 'time'
3
+ require 'digest/sha1'
4
+ require 'bencode'
5
+ require 'sinatra/base'
6
+
7
+ # This extension will serve up the contents of the specified folder as web seeded torrents.
8
+ # Both webseed versions are supported (shad0w's and GetRight's) and there is an inbuilt tracker
9
+ # with a modicum of intelligence, though an external tracker can be used if desired.
10
+
11
+ module Sinatra
12
+ module Torrent
13
+
14
+ # Options etc
15
+ def self.registered(app)
16
+ # Putting the annouce URL of a tracker in here will use that tracker rather than the inbuilt one
17
+ app.set :external_tracker, nil
18
+ # Directory which holds all the files which will be provided as torrents
19
+ app.set :downloads_directory, File.dirname(__FILE__)+'downloads'
20
+ # Mount point for the downloads directory
21
+ app.set :downloads_mount, 'downloads'
22
+ # Mount point for the torrents directory
23
+ app.set :torrents_mount, 'torrents'
24
+ # Load up a database adapter if one isn't already loaded
25
+ require 'sinatra/torrent/activerecord' unless (Sinatra::Torrent.const_defined?('Database') rescue false)
26
+ # Stores the instance of the database used to store tracker info.
27
+ app.set :database_adapter, Sinatra::Torrent::Database.new
28
+ # The comment added into torrents
29
+ app.set :torrent_comment, ''
30
+ # Do we wish to track external torrents too? (untested)
31
+ app.set :allow_external_torrents, false
32
+ # The frequency with which we ask trackers to announce themselves. Once every x seconds
33
+ app.set :announce_frequency, 30
34
+
35
+ # TORRENTS
36
+
37
+ app.mime_type :torrent, 'application/x-bittorrent'
38
+
39
+ # Serves up the torrents with appropriate announce URL
40
+ app.get Regexp.new("^/#{app.options.torrents_mount}/(.+)\.torrent$") do |rel_location|
41
+ filename = File.join(options.downloads_directory, rel_location)
42
+ halt(404, "That file doesn't exist! #{filename}") unless File.exists?(filename)
43
+
44
+ if true #!(d = options.database_adapter.torrent_by_path_and_timestamp(filename,File.mtime(filename)))
45
+
46
+ d = {
47
+ 'metadata' => {
48
+ # TODO: Version?
49
+ 'created by' => 'sinatra-torrent (0.0.1) (http://github.com/jphastings/sinatra-torrent)',
50
+ 'creation date' => Time.now.to_i,
51
+ 'info' => {
52
+ 'name' => File.basename(rel_location),
53
+ 'length' => File.size(filename),
54
+ 'piece length' => 2**10, # TODO: Choose reasonable piece size
55
+ 'pieces' => ''
56
+ }
57
+ }
58
+ }
59
+
60
+ begin
61
+ file = open(filename,'r')
62
+
63
+ Timeout::timeout(1) do
64
+ begin
65
+ d['metadata']['info']['pieces'] += Digest::SHA1.digest(file.read(d['metadata']['info']['piece length']))
66
+ end until file.eof?
67
+ end
68
+ rescue Timeout::Error
69
+ # TODO: Actually run it in the background!
70
+ halt(503,"This torrent is taking too long to build, we're [not currently supporting!] running it in the background. Please try again in a few minutes.")
71
+ ensure
72
+ file.close
73
+ end
74
+
75
+ d['infohash'] = Digest::SHA1.hexdigest(d['metadata']['info'].bencode)
76
+ options.database_adapter.store_torrent(filename,File.mtime(filename),d['metadata'],d['infohash'])
77
+ end
78
+
79
+ # These are options which could change between database retrievals
80
+ d['metadata'].merge!({
81
+ 'httpseeds' => [File.join('http://'+env['HTTP_HOST'],URI.encode(options.torrents_mount),'webseed')],
82
+ 'url-list' => [File.join('http://'+env['HTTP_HOST'],URI.encode(options.downloads_mount),URI.encode(rel_location)+'?'+d['infohash'])],
83
+ 'announce' => options.external_tracker || File.join('http://'+env['HTTP_HOST'],URI.encode(options.torrents_mount),'announce'),
84
+ 'comment' => options.torrent_comment,
85
+ })
86
+
87
+ content_type :torrent, :charset => 'utf-8'
88
+ d['metadata'].bencode
89
+ end
90
+
91
+ # TRACKER
92
+
93
+ # Tracker announce mount point
94
+ app.get "/#{app.options.torrents_mount}/announce" do
95
+ # Convert to a hex info_hash if required TODO: Is it required?
96
+ params['info_hash'] = Digest.hexencode(params['info_hash'] || '')
97
+ halt(400,"A valid info-hash was not given") if params['info_hash'].match(/^[0-9a-f]{40}$/).nil?
98
+ info = options.database_adapter.torrent_info(params['info_hash'])
99
+
100
+ if (!options.allow_external_torrents and !options.database_adapter.torrent_by_infohash(params['info_hash']))
101
+ return {
102
+ 'failure reason' => 'This tracker does not track that torrent'
103
+ }.bencode
104
+ end
105
+
106
+ # TODO: Validation
107
+
108
+ params['ip'] ||= env['REMOTE_ADDR']
109
+
110
+ # Errmmm - HACK!
111
+ params['peer_id'] = params['peer_id'].force_encoding("ISO-8859-1")
112
+
113
+ # Registers this peer's announcement
114
+ options.database_adapter.announce(params)
115
+
116
+ {
117
+ 'interval' => options.announce_frequency,
118
+ #'tracker id' => 'bleugh', # TODO: Keep this?
119
+ 'complete' => info['complete'],
120
+ 'incomplete' => info['incomplete'],
121
+ 'peers' => options.database_adapter.peers_by_infohash(params['info_hash'],[params['peer_id']],(params['numwant'] || 50).to_i),
122
+ }.bencode
123
+ end
124
+
125
+ # TODO: Scrape
126
+ app.get '/torrents/scrape' do
127
+ # TODO: Make it work!
128
+ end
129
+
130
+ # INDEX PAGE
131
+ app.get "/#{app.options.torrents_mount}/" do
132
+ haml :torrents_index,:locals => {:torrents => Dir.glob("#{options.downloads_directory}/**").collect {|f| f[options.downloads_directory.length+1..-1] } }
133
+ end
134
+
135
+ # DATA
136
+
137
+ # BitTornado WebSeeding manager
138
+ app.get "/#{app.options.torrents_mount}/webseed" do
139
+ # Which file is the client looking for?
140
+ halt(404, "Torrent not tracked") unless (options.database_adapter.torrent_by_infohash(params[:infohash]))
141
+
142
+ # http://bittornado.com/docs/webseed-spec.txt
143
+
144
+ # TODO: intelligent wait period
145
+ halt(503,"15") if false # ask clients to wait 15 seconds before requesting again
146
+ end
147
+
148
+ # Provides the files for web download. Any query parameters are treated as a checksum for the file (via the torrent infohash)
149
+ app.get "/#{app.options.downloads_mount}/:filename" do
150
+ filename = File.join(options.downloads_directory,File.expand_path('/'+params[:filename]))
151
+ halt(404) unless File.exists?(filename)
152
+
153
+ # If there are query params then we assume it's specifying a specific version of the file by info_hash
154
+ halt(409,"The file is no longer the same as the one specified in your torrent") if !env['QUERY_STRING'].empty? and (options.database_adapter.torrent_by_path_and_timestamp(filename,File.mtime(filename))['infohash'] rescue nil) != env['QUERY_STRING']
155
+ send_file(filename)
156
+ end
157
+ end
158
+ end
159
+
160
+ register Torrent
161
+ end
@@ -0,0 +1,143 @@
1
+ # The active record adapter works with SQLite3 in memory by default
2
+ require 'active_record'
3
+
4
+ module Sinatra
5
+ module Torrent
6
+ # This is the wrapper class used by sinatra-torrent to communicate with
7
+ # any database system
8
+ class Database
9
+ # Default settings for the connection
10
+ @@settings = {
11
+ 'adapter' => 'sqlite3',
12
+ 'database' => ':memory:'
13
+ }
14
+
15
+ # Allows the set up of Active Record
16
+ def self.settings=(settings)
17
+ raise ArgumentError if !settings.is_a?(Hash)
18
+ @@settings = settings
19
+ end
20
+
21
+ # Makes sure the table is present & ready, log in using the settings
22
+ def initialize
23
+ @db = ActiveRecord::Base.establish_connection(@@settings)
24
+
25
+ unless Torrent.table_exists?
26
+ ActiveRecord::Schema.define do
27
+ create_table :torrents do |table|
28
+ table.text :path, :null => false
29
+ table.text :metadata, :null => false
30
+ table.string :infohash, :null => false
31
+ table.timestamp :timestamp, :null => false
32
+ end
33
+
34
+ add_index :torrents, :infohash, :unique => true
35
+
36
+ create_table :peers do |table|
37
+ table.string :torrent_infohash, :null => false
38
+ table.string :peer_id
39
+ table.integer :port
40
+ table.integer :uploaded
41
+ table.integer :downloaded
42
+ table.integer :left
43
+ table.string :ip
44
+
45
+ table.timestamps
46
+ end
47
+
48
+ add_index :peers, [:peer_id,:torrent_infohash],:unique => true
49
+ end
50
+ end
51
+ end
52
+
53
+ # Stores a torrent in the database
54
+ def store_torrent(path,timestamp,metadata,infohash)
55
+ # Make sure this path and this infohash don't already exist
56
+ Torrent.find_by_path_and_timestamp(path,timestamp).delete rescue nil
57
+ Torrent.find_by_infohash(infohash).delete rescue nil
58
+
59
+ Torrent.new(
60
+ :path => path,
61
+ :metadata => metadata,
62
+ :infohash => infohash,
63
+ :timestamp => timestamp
64
+ ).save
65
+ end
66
+
67
+ # Find a torrent by infohash
68
+ def torrent_by_infohash(infohash)
69
+ torrent = Torrent.find_by_infohash(infohash)
70
+ return false if torrent.nil?
71
+
72
+ {
73
+ 'metadata' => torrent.metadata,
74
+ 'infohash' => torrent.infohash,
75
+ 'path' => torrent.path,
76
+ 'timestamp'=> torrent.timestamp
77
+ }
78
+ end
79
+
80
+ # Finds a torrent by
81
+ def torrent_by_path_and_timestamp(path,timestamp)
82
+ torrent = Torrent.find_by_path_and_timestamp(path,timestamp)
83
+ return false if torrent.nil?
84
+
85
+ {
86
+ 'metadata' => torrent.metadata,
87
+ 'infohash' => torrent.infohash,
88
+ 'path' => torrent.path,
89
+ 'timestamp'=> torrent.timestamp
90
+ }
91
+ end
92
+
93
+ # Lists the currently registered peers for a given torrent
94
+ # if peer_ids is populated with any peer ids then they will be excluded from the list
95
+ def peers_by_infohash(infohash, peer_ids = [], peers = 50)
96
+ begin
97
+ # TODO: Random order & actual number of peers (if peer_ids is in returned amount)
98
+ Peer.find_by_torrent_infohash(infohash,:limit => peers).delete_if {|peer| peer_ids.include? peer.peer_id}.map do |peer|
99
+ {
100
+ 'peer id' => peer.peer_id,
101
+ 'ip' => peer.ip,
102
+ 'port' => peer.port
103
+ }
104
+ end
105
+ rescue NoMethodError
106
+ []
107
+ end
108
+ end
109
+
110
+ # Returns information about the torrent as provided by it's infohash
111
+ def torrent_info(infohash)
112
+ info = Peer.find_by_sql(["SELECT (SELECT COUNT(*) FROM 'peers' WHERE `left` != 0 AND `torrent_infohash` = ?) as `incomplete`, (SELECT COUNT(*) FROM 'peers' WHERE `left` == 0 AND `torrent_infohash` = ?) as `complete`",infohash, infohash])[0]
113
+
114
+ {
115
+ 'complete' => info.complete,
116
+ 'incomplete' => info.incomplete
117
+ }
118
+ end
119
+
120
+ # Announce!
121
+ def announce(params)
122
+ peer = Peer.find_or_create_by_torrent_infohash_and_peer_id(params['info_hash'],params['peer_id'])
123
+
124
+ peer.ip ||= params['ip']
125
+ peer.port ||= params['port']
126
+ peer.uploaded = params['uploaded']
127
+ peer.downloaded = params['downloaded']
128
+ peer.left = params['left']
129
+
130
+ peer.save
131
+ end
132
+
133
+ private
134
+ class Torrent < ActiveRecord::Base
135
+ serialize :metadata, Hash
136
+ end
137
+
138
+ class Peer < ActiveRecord::Base
139
+
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,43 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{sinatra-torrent}
8
+ s.version = "0.0.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["JP Hastings-Spital"]
12
+ s.date = %q{2011-02-08}
13
+ s.description = %q{An extension to Sinatra which will allow you to run a webseeded torrent tracker of files in the folder you specify.}
14
+ s.email = %q{jphastings@gmail.com}
15
+ s.files = [
16
+ "Rakefile",
17
+ "Readme.md",
18
+ "VERSION",
19
+ "lib/sinatra/torrent.rb",
20
+ "lib/sinatra/torrent/activerecord.rb",
21
+ "sinatra-torrent.gemspec",
22
+ "views/torrents_index.haml"
23
+ ]
24
+ s.homepage = %q{http://github.com/jphastings/sinatra-torrent}
25
+ s.rdoc_options = ["--charset=UTF-8"]
26
+ s.require_paths = ["lib"]
27
+ s.rubygems_version = %q{1.3.7}
28
+ s.summary = %q{A sinatra extension to run webseeded torrent tracker}
29
+
30
+ if s.respond_to? :specification_version then
31
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
32
+ s.specification_version = 3
33
+
34
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
35
+ s.add_runtime_dependency(%q<sinatra>, [">= 1.1.2"])
36
+ else
37
+ s.add_dependency(%q<sinatra>, [">= 1.1.2"])
38
+ end
39
+ else
40
+ s.add_dependency(%q<sinatra>, [">= 1.1.2"])
41
+ end
42
+ end
43
+
@@ -0,0 +1,5 @@
1
+ %ul
2
+ -locals[:torrents].each do |torrent|
3
+ %li
4
+ %a{:href=>"/#{options.torrents_mount}/#{torrent}.torrent"}=torrent
5
+
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-torrent
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 3
10
+ version: 0.0.3
11
+ platform: ruby
12
+ authors:
13
+ - JP Hastings-Spital
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-08 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: sinatra
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 23
30
+ segments:
31
+ - 1
32
+ - 1
33
+ - 2
34
+ version: 1.1.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: An extension to Sinatra which will allow you to run a webseeded torrent tracker of files in the folder you specify.
38
+ email: jphastings@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - Rakefile
47
+ - Readme.md
48
+ - VERSION
49
+ - lib/sinatra/torrent.rb
50
+ - lib/sinatra/torrent/activerecord.rb
51
+ - sinatra-torrent.gemspec
52
+ - views/torrents_index.haml
53
+ has_rdoc: true
54
+ homepage: http://github.com/jphastings/sinatra-torrent
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options:
59
+ - --charset=UTF-8
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.7
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: A sinatra extension to run webseeded torrent tracker
87
+ test_files: []
88
+