Module: Sinatra::Torrent

Defined in:
lib/sinatra/torrent.rb,
lib/sinatra/torrent/helpers.rb,
lib/sinatra/torrent/activerecord.rb

Defined Under Namespace

Classes: Database

Constant Summary collapse

@@downloads_directory =
'downloads'

Class Method Summary collapse

Class Method Details

.create(filename) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/sinatra/torrent/helpers.rb', line 22

def self.create(filename)
  d = {
    'metadata' => {
      'created by' => "sinatra-torrent (#{File.read(File.expand_path(File.join(__FILE__,'..','..','..','..','VERSION'))).strip}) (http://github.com/jphastings/sinatra-torrent)",
      'creation date' => Time.now.to_i,
      'info' => {
        'name' => File.basename(filename),
        'length' => File.size(filename),
        'piece length' => 2**10, # TODO: Choose reasonable piece size
        'pieces' => ''
      }
    }
  }

  begin
    file = open(filename,'r')

    begin
      d['metadata']['info']['pieces'] += Digest::SHA1.digest(file.read(d['metadata']['info']['piece length']))
    end until file.eof?
  ensure
    file.close
  end

  d['infohash'] = Digest::SHA1.hexdigest(d['metadata']['info'].bencode)

  return d
end

.downloads_directoryObject



18
19
20
# File 'lib/sinatra/torrent/helpers.rb', line 18

def self.downloads_directory
  @@downloads_directory
end

.downloads_directory=(d) ⇒ Object



9
10
11
12
13
14
15
16
# File 'lib/sinatra/torrent/helpers.rb', line 9

def self.downloads_directory=(d)
  if File.directory?(d)
    @@downloads_directory = d
  else
    # TODO: ERR error
    raise RuntimeError, "The downloads directory doesn't exist"
  end
end

.registered(app) ⇒ Object

settings etc



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/sinatra/torrent.rb', line 25

def self.registered(app)
  # Set default settings
  begin @@settings rescue self.settings=({}) end
  # Putting the annouce URL of a tracker in here will use that tracker rather than the inbuilt one
  app.set :external_tracker, nil
  # Load up a database adapter if one isn't already loaded
  require 'sinatra/torrent/activerecord' unless (Sinatra::Torrent.const_defined?('Database') rescue false)
  # Stores the instance of the database used to store tracker info.
  app.set :database_adapter, Sinatra::Torrent::Database.new
  # The comment added into torrents
  app.set :torrent_comment, ''
  # Do we wish to track external torrents too? (untested)
  app.set :allow_external_torrents, false
  # The frequency with which we ask trackers to announce themselves. Once every x seconds
  app.set :announce_frequency, 900
  # Method to call when torrent creation timesout
  app.set :torrent_timeout, nil
  
# TORRENTS

  app.mime_type :torrent, 'application/x-bittorrent'

  # Serves up the torrents with appropriate announce URL
  app.get Regexp.new("^/#{@@settings[:torrents_mount]}/(.+)\.torrent$") do |rel_location|
    filename = File.join(@@settings[:downloads_directory], rel_location)
    halt(404, "That file doesn't exist! #{filename}") unless File.exists?(filename)
    
    if !(d = settings.database_adapter.torrent_by_path_and_timestamp(rel_location,File.mtime(filename)))
      begin
        Timeout::timeout(1) do
          d = Sinatra::Torrent.create(filename)
        end
      rescue Timeout::Error
        eta = settings.database_adapter.add_hashjob(rel_location)
        
        begin
          wait = case (eta/60).floor
          when 0
            'under a minute'
          when 1
            'about a minute'
          else
            "about #{(eta/60).floor} minutes"
          end
        rescue NoMethodError
          wait = "a short while"
        end
        
        settings.torrent_timeout.call if settings.torrent_timeout.is_a?(Proc)
        
        halt(503,"This torrent is taking too long to build, we're running it in the background. Please try again in #{wait}.")
      end
      
      settings.database_adapter.store_torrent(rel_location,File.mtime(filename),d['metadata'],d['infohash'])
    end
    
    # These are settings which could change between database retrievals
    d['metadata'].merge!({
# Webseeds not currently supported
#          'httpseeds' => [File.join('http://'+env['HTTP_HOST'],URI.encode(settings.torrents_mount),'webseed')],
      'url-list' => [File.join('http://'+env['HTTP_HOST'],URI.encode(@@settings[:downloads_mount]),URI.encode(rel_location)+'?'+d['infohash'])],
      'announce' => settings.external_tracker || File.join('http://'+env['HTTP_HOST'],URI.encode(@@settings[:torrents_mount]),'announce'),
      'comment' => settings.torrent_comment,
    })
    
    content_type :torrent, :charset => 'utf-8'
    d['metadata'].bencode
  end
  
# TRACKER
  
  # Tracker announce mount point
  app.get "/#{@@settings[:torrents_mount]}/announce" do
    # Convert to a hex info_hash if required TODO: Is it required?
    params['info_hash'] = Digest.hexencode(params['info_hash'] || '')
    halt(400,"A valid info-hash was not given") if params['info_hash'].match(/^[0-9a-f]{40}$/).nil?
    info = settings.database_adapter.torrent_info(params['info_hash'])
    
    if (!settings.allow_external_torrents and !settings.database_adapter.torrent_by_infohash(params['info_hash']))
      return {
        'failure reason' => 'This tracker does not track that torrent'
      }.bencode
    end
    
    # TODO: Validation
    
    params['ip'] ||= env['REMOTE_ADDR']
    
    # Errmmm - HACK!
    params['peer_id'] = params['peer_id'].force_encoding("ISO-8859-1")
    
    # Registers this peer's announcement
    settings.database_adapter.announce(params)

    {
      'interval' => settings.announce_frequency,
      #'tracker id' => 'bleugh', # TODO: Keep this?
      'complete' => info['complete'],
      'incomplete' => info['incomplete'],
      'peers' => settings.database_adapter.peers_by_infohash(params['info_hash'],[params['peer_id']],(params['numwant'] || 50).to_i),
    }.bencode
  end
  
  # TODO: Scrape
  app.get '/torrents/scrape' do
    {
      'files' => Hash[*request.env['QUERY_STRING'].scan(/info_hash=([0-9a-f]{20})/).collect do |infohash|
        torrent = settings.database_adapter.torrent_by_infohash(infohash[0])
        next if !torrent
        stats = settings.database_adapter.torrent_info(infohash[0])
        [
          torrent['infohash'],
          {
            'complete'   => stats['complete'],
            'downloaded' => 0,
            'incomplete' => stats['incomplete'],
            'name'       => File.basename(torrent['path'])
          }
        ]
      end.compact.flatten]
    }.bencode
  end

# INDEX PAGE

  app.get "/#{@@settings[:torrents_mount]}/" do
    locals = {:torrents => (Dir.glob("#{@@settings[:downloads_directory]}/**").collect {|f| {:file => f[@@settings[:downloads_directory].length+1..-1],:hashed? => settings.database_adapter.torrent_by_path_and_timestamp(f[@@settings[:downloads_directory].length+1..-1],File.mtime(f)) != false} } rescue [])}
    begin
      haml :torrents_index,:locals => locals
    rescue Errno::ENOENT
      "<ul>"<<locals[:torrents].collect{|t| "<li><a href=\"/#{@@settings[:torrents_mount]}/#{t[:file]}.torrent\" class=\"#{(t[:hashed?] ? 'ready' : 'unhashed')}\">#{t[:file]}</a></li>" }.join<<"</ul>"
    end
  end

# DATA

=begin : Not currently supported
  # BitTornado WebSeeding manager
  # http://bittornado.com/docs/webseed-spec.txt
  app.get "/#{settings.torrents_mount}/webseed" do
    # Which file is the client looking for?
    halt(404, "Torrent not tracked") unless (d = settings.database_adapter.torrent_by_infohash(params[:infohash]))
    
    
  end
=end
  
  # Provides the files for web download. Any query parameters are treated as a checksum for the file (via the torrent infohash)
  app.get "/#{@@settings[:downloads_mount]}/:filename" do
    filename = File.join(@@settings[:downloads_directory],File.expand_path('/'+params[:filename]))
    halt(404) unless File.exists?(filename)
    
    # If there are query params then we assume it's specifying a specific version of the file by info_hash
    halt(409,"The file is no longer the same as the one specified in your torrent") if !env['QUERY_STRING'].empty? and (settings.database_adapter.torrent_by_path_and_timestamp(filename,File.mtime(filename))['infohash'] rescue nil) != env['QUERY_STRING'] 
    send_file(filename)
  end
end

.settings=(settings = {}) ⇒ Object

Sets the settings for the app



12
13
14
15
16
17
18
19
20
21
22
# File 'lib/sinatra/torrent.rb', line 12

def self.settings=(settings = {})
  @@settings = {
    # Directory which holds all the files which will be provided as torrents
    :downloads_directory => File.join(File.dirname(__FILE__),Sinatra::Torrent.downloads_directory),
    # Mount point for the downloads directory
    :downloads_mount => 'downloads',
    # Mount point for the torrents directory
    :torrents_mount => 'torrents'
  }
  settings.each { |key,value| @@settings[key] = value unless @@settings[value].nil? }
end