quartz_flow 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/etc/quartz.rb CHANGED
@@ -7,10 +7,10 @@ set :bind, "0.0.0.0"
7
7
  set :port, 4444
8
8
 
9
9
  # Directory where downloaded torrent data will be stored
10
- set :basedir, "/mnt/twotb/movies"
10
+ set :basedir, "download"
11
11
 
12
12
  # Directory where .torrent files and .info files will be stored.
13
- set :metadir, "meta"
13
+ set :metadir, "meta"
14
14
 
15
15
  # TCP port used for torrents
16
16
  set :torrent_port, 9997
@@ -23,3 +23,10 @@ set :torrent_log, "log/torrent.log"
23
23
 
24
24
  # On which day of the month should monthly usage tracking reset
25
25
  set :monthly_usage_reset_day, 5
26
+
27
+ # Torrent Queueing settings.
28
+ # Max number of active torrents is the max number of torrents that can be running at once.
29
+ # Max number of incomplete torrents is a subset of the max active torrents, and describes
30
+ # the max number of torrents that can be running that are not uploading.
31
+ set :torrent_queue_max_active, 10
32
+ set :torrent_queue_max_incomplete, 5
@@ -46,6 +46,8 @@ class Server < Sinatra::Base
46
46
  set :password_file, "etc/passwd"
47
47
  set :logging, true
48
48
  set :monthly_usage_reset_day, 1
49
+ set :torrent_queue_max_incomplete, 5
50
+ set :torrent_queue_max_active, 10
49
51
 
50
52
  # Load configuration settings
51
53
  eval File.open("./etc/quartz.rb","r").read
@@ -72,9 +74,13 @@ class Server < Sinatra::Base
72
74
  setDefaultLevel :info
73
75
  end
74
76
  LogConfigurator.configLevels
75
- peerClient = QuartzTorrent::PeerClient.new(settings.basedir)
77
+ peerClient = QuartzTorrent::PeerClient.new(settings.basedir, settings.torrent_queue_max_incomplete, settings.torrent_queue_max_active)
76
78
  peerClient.port = settings.torrent_port
77
- peerClient.start
79
+ begin
80
+ peerClient.start
81
+ rescue Errno::EADDRINUSE
82
+ raise "Starting torrent peer failed because listening on port #{settings.torrent_port} failed: " + $!.message
83
+ end
78
84
 
79
85
  # Initialize Datamapper
80
86
  #DataMapper::Logger.new($stdout, :debug)
@@ -211,13 +217,15 @@ class Server < Sinatra::Base
211
217
  begin
212
218
  $manager.storeMagnet(magnet)
213
219
  rescue
220
+ puts "Storing magnet link failed: #{$!}"
221
+ puts $!.backtrace.join("\n")
214
222
  halt 500, "Storing magnet link failed: #{$!}."
215
223
  end
216
224
 
217
225
  begin
218
226
  $manager.startMagnet(magnet)
219
227
  rescue
220
- puts $!
228
+ puts "Starting magnet link failed: #{$!}"
221
229
  puts $!.backtrace.join("\n")
222
230
  halt 500, "Starting magnet link failed: #{$!}."
223
231
  end
@@ -254,7 +262,7 @@ class Server < Sinatra::Base
254
262
 
255
263
  infoHash = json["infohash"]
256
264
  halt 500, "Pausing torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
257
- $manager.peerClient.setPaused QuartzTorrent::hexToBytes(infoHash), true
265
+ $manager.setTorrentPaused infoHash, true
258
266
  puts "Pausing torrent"
259
267
  "OK"
260
268
  end
@@ -264,7 +272,7 @@ class Server < Sinatra::Base
264
272
 
265
273
  infoHash = json["infohash"]
266
274
  halt 500, "Unpausing torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
267
- $manager.peerClient.setPaused QuartzTorrent::hexToBytes(infoHash), false
275
+ $manager.setTorrentPaused infoHash, false
268
276
  "OK"
269
277
  end
270
278
 
@@ -103,6 +103,24 @@ class SettingsHelper
103
103
  @@saveFilterForDuration,
104
104
  Proc.new{ |v| QuartzTorrent::Formatter.formatTime(v) }
105
105
  ),
106
+ :paused => SettingMetainfo.new(
107
+ :paused,
108
+ :torrent,
109
+ Proc.new{ |v| v.to_s },
110
+ Proc.new{ |v| v.downcase == "true" }
111
+ ),
112
+ :bytesUploaded => SettingMetainfo.new(
113
+ :bytesUploaded,
114
+ :torrent,
115
+ Proc.new{ |v| v.to_s },
116
+ Proc.new{ |v| v.to_i }
117
+ ),
118
+ :bytesDownloaded => SettingMetainfo.new(
119
+ :bytesDownloaded,
120
+ :torrent,
121
+ Proc.new{ |v| v.to_s},
122
+ Proc.new{ |v| v.to_i }
123
+ ),
106
124
  }
107
125
 
108
126
  def set(settingName, value, owner = nil)
@@ -8,6 +8,7 @@ class TorrentManager
8
8
  def initialize(peerClient, torrentFileDir, monthlyResetDay)
9
9
  @peerClient = peerClient
10
10
  @cachedTorrentData = nil
11
+ @cachedTorrentDataMutex = Mutex.new
11
12
  @cachedAt = nil
12
13
  @cacheLifetime = 2
13
14
  @torrentFileDir = torrentFileDir
@@ -15,22 +16,28 @@ class TorrentManager
15
16
  @usageTracker = UsageTracker.new(monthlyResetDay)
16
17
  # Start a thread to keep track of usage.
17
18
  startUsageTrackerThread
19
+ startTorrentDataThread
18
20
  end
19
21
 
20
22
  attr_reader :peerClient
21
23
 
22
24
  def torrentData(infoHash = nil)
23
- if (! @cachedTorrentData || Time.new - @cachedAt > @cacheLifetime) && ! @peerClientStopped
24
- @cachedTorrentData = @peerClient.torrentData
25
- @cachedAt = Time.new
25
+ result = nil
26
+
27
+ # The first time, we may need to wait for the thread to load the data.
28
+ sleep(0.25) while ! @cachedTorrentData
29
+
30
+ @cachedTorrentDataMutex.synchronize do
31
+ result = @cachedTorrentData
26
32
  end
27
-
28
- @cachedTorrentData
33
+ result
29
34
  end
30
35
 
31
36
  def stopPeerClient
32
37
  @peerClient.stop
33
38
  @peerClientStopped = true
39
+ stopTorrentDataThread
40
+ stopUsageTrackerThread
34
41
  end
35
42
 
36
43
  # Start torrents that already exist in the torrent file directory
@@ -102,11 +109,15 @@ class TorrentManager
102
109
  h[:uploadRateLimit] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRateLimit])
103
110
  h[:downloadRateLimit] = QuartzTorrent::Formatter.formatSize(h[:downloadRateLimit])
104
111
  h[:bytesUploaded] = QuartzTorrent::Formatter.formatSize(h[:bytesUploaded])
112
+ h[:bytesDownloaded] = QuartzTorrent::Formatter.formatSize(h[:bytesDownloaded])
105
113
  h[:uploadDuration] = QuartzTorrent::Formatter.formatTime(h[:uploadDuration]) if h[:uploadDuration]
106
114
 
107
115
  h[:completePieces] = d.completePieceBitfield ? d.completePieceBitfield.countSet : 0
108
116
  h[:totalPieces] = d.completePieceBitfield ? d.completePieceBitfield.length : 0
109
117
 
118
+ # Only send 8 alarms
119
+ h[:alarms] = h[:alarms].first(8)
120
+
110
121
  if where
111
122
  matches = true
112
123
  where.each do |k,v|
@@ -222,6 +233,15 @@ class TorrentManager
222
233
  FileUtils.rm magnetFile if File.exists?(magnetFile)
223
234
  end
224
235
 
236
+ # Pause or unpause the specified torrent. Store the pause state in the database.
237
+ def setTorrentPaused(infoHash, val)
238
+ infoHashBytes = QuartzTorrent::hexToBytes(infoHash)
239
+ @peerClient.setPaused infoHashBytes, val
240
+
241
+ helper = SettingsHelper.new
242
+ helper.set :paused, val, infoHash
243
+ end
244
+
225
245
  # Update the torrent settings (upload rate limit, etc) from database values
226
246
  def applyTorrentSettings(infoHash)
227
247
  asciiInfoHash = QuartzTorrent::bytesToHex(infoHash)
@@ -241,10 +261,18 @@ class TorrentManager
241
261
  uploadDuration = helper.get(:defaultUploadDuration, :unfiltered) if ! uploadDuration
242
262
  uploadDuration = uploadDuration.to_i if uploadDuration
243
263
 
264
+ paused = helper.get(:paused, :filter, asciiInfoHash)
265
+
266
+ bytesDownloaded = helper.get(:bytesDownloaded, :filter, asciiInfoHash)
267
+ bytesUploaded = helper.get(:bytesUploaded, :filter, asciiInfoHash)
268
+
244
269
  @peerClient.setUploadRateLimit infoHash, uploadRateLimit
245
270
  @peerClient.setDownloadRateLimit infoHash, downloadRateLimit
246
271
  @peerClient.setUploadRatio infoHash, ratio
247
272
  @peerClient.setUploadDuration infoHash, uploadDuration
273
+ @peerClient.setPaused infoHash, paused
274
+ @peerClient.adjustBytesDownloaded infoHash, bytesDownloaded if bytesDownloaded
275
+ @peerClient.adjustBytesUploaded infoHash, bytesUploaded if bytesUploaded
248
276
  end
249
277
 
250
278
  # Get the usage for the current period of the specified type.
@@ -274,6 +302,7 @@ class TorrentManager
274
302
  QuartzTorrent.initThread("torrent_usage_tracking")
275
303
 
276
304
  Thread.current[:stopped] = false
305
+ helper = SettingsHelper.new
277
306
 
278
307
  while ! Thread.current[:stopped]
279
308
  begin
@@ -282,6 +311,9 @@ class TorrentManager
282
311
  usage = 0
283
312
  torrentData.each do |k,v|
284
313
  usage += v.bytesUploaded + v.bytesDownloaded
314
+ asciiInfoHash = QuartzTorrent::bytesToHex(k)
315
+ helper.set :bytesDownloaded, v.bytesDownloaded, asciiInfoHash
316
+ helper.set :bytesUploaded, v.bytesUploaded, asciiInfoHash
285
317
  end
286
318
  @usageTracker.update(usage)
287
319
  rescue
@@ -291,4 +323,42 @@ class TorrentManager
291
323
  end
292
324
  end
293
325
  end
326
+
327
+ def stopUsageTrackerThread
328
+ @usageTrackerThread[:stopped] = true
329
+ end
330
+
331
+ # Start a thread to update torrent data
332
+ def startTorrentDataThread
333
+ @torrentDataThread = Thread.new do
334
+ QuartzTorrent.initThread("torrent_data_loader")
335
+
336
+ Thread.current[:stopped] = false
337
+
338
+ while ! Thread.current[:stopped]
339
+ begin
340
+ timer = Time.new
341
+ if (! @cachedTorrentData || Time.new - @cachedAt > @cacheLifetime) && ! @peerClientStopped
342
+ data = @peerClient.torrentData
343
+ @cachedTorrentDataMutex.synchronize do
344
+ @cachedTorrentData = data
345
+ @cachedAt = Time.new
346
+ end
347
+ end
348
+ timer = Time.new - timer
349
+
350
+ if timer < @cacheLifetime
351
+ sleep @cacheLifetime-timer
352
+ end
353
+ rescue
354
+ puts "Error in torrent data loader thread: #{$!}"
355
+ puts $!.backtrace.join "\n"
356
+ end
357
+ end
358
+ end
359
+ end
360
+
361
+ def stopTorrentDataThread
362
+ @torrentDataThread[:stopped] = true
363
+ end
294
364
  end
@@ -191,7 +191,9 @@ class MonthlyBucketChangeCriteria < BucketChangeCriteria
191
191
  def criteriaData
192
192
  now = Time.new
193
193
  nextMonth = now.mon % 12 + 1
194
- Time.local(now.year, nextMonth, @resetDay)
194
+ year = now.year
195
+ year += 1 if nextMonth == 1
196
+ Time.local(year, nextMonth, @resetDay)
195
197
  end
196
198
  end
197
199
 
@@ -51,6 +51,15 @@ module QuartzTorrent
51
51
  end
52
52
  end
53
53
 
54
+ class Alarm
55
+ def to_h
56
+ result = {}
57
+ result[:details] = @details
58
+ result[:time] = @time
59
+ result
60
+ end
61
+ end
62
+
54
63
  class TorrentDataDelegate
55
64
  # Convert to a hash. Also flattens some of the data into new fields.
56
65
  def to_h
@@ -70,6 +79,8 @@ module QuartzTorrent
70
79
  0
71
80
  end
72
81
  end
82
+ # Cap estimated time at 9999 hours
83
+ secondsLeft = 35996400 if secondsLeft > 35996400
73
84
  result[:timeLeft] = Formatter.formatTime(secondsLeft)
74
85
 
75
86
  ## Regular fields
@@ -87,12 +98,14 @@ module QuartzTorrent
87
98
  result[:metainfoLength] = @metainfoLength
88
99
  result[:metainfoCompletedLength] = @metainfoCompletedLength
89
100
  result[:paused] = @paused
101
+ result[:queued] = @queued
90
102
  result[:uploadRateLimit] = @uploadRateLimit
91
103
  result[:downloadRateLimit] = @downloadRateLimit
92
104
  result[:ratio] = @ratio
93
105
  result[:uploadDuration] = @uploadDuration
94
106
  result[:bytesUploaded] = @bytesUploaded
95
107
  result[:bytesDownloaded] = @bytesDownloaded
108
+ result[:alarms] = @alarms.collect{ |a| a.to_h }
96
109
 
97
110
  result
98
111
  end
@@ -0,0 +1,457 @@
1
+ /**
2
+ * @license AngularJS v1.0.8
3
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {
7
+ 'use strict';
8
+
9
+ /**
10
+ * @ngdoc overview
11
+ * @name ngResource
12
+ * @description
13
+ */
14
+
15
+ /**
16
+ * @ngdoc object
17
+ * @name ngResource.$resource
18
+ * @requires $http
19
+ *
20
+ * @description
21
+ * A factory which creates a resource object that lets you interact with
22
+ * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
23
+ *
24
+ * The returned resource object has action methods which provide high-level behaviors without
25
+ * the need to interact with the low level {@link ng.$http $http} service.
26
+ *
27
+ * # Installation
28
+ * To use $resource make sure you have included the `angular-resource.js` that comes in Angular
29
+ * package. You can also find this file on Google CDN, bower as well as at
30
+ * {@link http://code.angularjs.org/ code.angularjs.org}.
31
+ *
32
+ * Finally load the module in your application:
33
+ *
34
+ * angular.module('app', ['ngResource']);
35
+ *
36
+ * and you are ready to get started!
37
+ *
38
+ * @param {string} url A parameterized URL template with parameters prefixed by `:` as in
39
+ * `/user/:username`. If you are using a URL with a port number (e.g.
40
+ * `http://example.com:8080/api`), you'll need to escape the colon character before the port
41
+ * number, like this: `$resource('http://example.com\\:8080/api')`.
42
+ *
43
+ * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
44
+ * `actions` methods.
45
+ *
46
+ * Each key value in the parameter object is first bound to url template if present and then any
47
+ * excess keys are appended to the url search query after the `?`.
48
+ *
49
+ * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
50
+ * URL `/path/greet?salutation=Hello`.
51
+ *
52
+ * If the parameter value is prefixed with `@` then the value of that parameter is extracted from
53
+ * the data object (useful for non-GET operations).
54
+ *
55
+ * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
56
+ * default set of resource actions. The declaration should be created in the following format:
57
+ *
58
+ * {action1: {method:?, params:?, isArray:?},
59
+ * action2: {method:?, params:?, isArray:?},
60
+ * ...}
61
+ *
62
+ * Where:
63
+ *
64
+ * - `action` – {string} – The name of action. This name becomes the name of the method on your
65
+ * resource object.
66
+ * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
67
+ * and `JSONP`
68
+ * - `params` – {object=} – Optional set of pre-bound parameters for this action.
69
+ * - isArray – {boolean=} – If true then the returned object for this action is an array, see
70
+ * `returns` section.
71
+ *
72
+ * @returns {Object} A resource "class" object with methods for the default set of resource actions
73
+ * optionally extended with custom `actions`. The default set contains these actions:
74
+ *
75
+ * { 'get': {method:'GET'},
76
+ * 'save': {method:'POST'},
77
+ * 'query': {method:'GET', isArray:true},
78
+ * 'remove': {method:'DELETE'},
79
+ * 'delete': {method:'DELETE'} };
80
+ *
81
+ * Calling these methods invoke an {@link ng.$http} with the specified http method,
82
+ * destination and parameters. When the data is returned from the server then the object is an
83
+ * instance of the resource class. The actions `save`, `remove` and `delete` are available on it
84
+ * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
85
+ * read, update, delete) on server-side data like this:
86
+ * <pre>
87
+ var User = $resource('/user/:userId', {userId:'@id'});
88
+ var user = User.get({userId:123}, function() {
89
+ user.abc = true;
90
+ user.$save();
91
+ });
92
+ </pre>
93
+ *
94
+ * It is important to realize that invoking a $resource object method immediately returns an
95
+ * empty reference (object or array depending on `isArray`). Once the data is returned from the
96
+ * server the existing reference is populated with the actual data. This is a useful trick since
97
+ * usually the resource is assigned to a model which is then rendered by the view. Having an empty
98
+ * object results in no rendering, once the data arrives from the server then the object is
99
+ * populated with the data and the view automatically re-renders itself showing the new data. This
100
+ * means that in most case one never has to write a callback function for the action methods.
101
+ *
102
+ * The action methods on the class object or instance object can be invoked with the following
103
+ * parameters:
104
+ *
105
+ * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
106
+ * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
107
+ * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
108
+ *
109
+ *
110
+ * @example
111
+ *
112
+ * # Credit card resource
113
+ *
114
+ * <pre>
115
+ // Define CreditCard class
116
+ var CreditCard = $resource('/user/:userId/card/:cardId',
117
+ {userId:123, cardId:'@id'}, {
118
+ charge: {method:'POST', params:{charge:true}}
119
+ });
120
+
121
+ // We can retrieve a collection from the server
122
+ var cards = CreditCard.query(function() {
123
+ // GET: /user/123/card
124
+ // server returns: [ {id:456, number:'1234', name:'Smith'} ];
125
+
126
+ var card = cards[0];
127
+ // each item is an instance of CreditCard
128
+ expect(card instanceof CreditCard).toEqual(true);
129
+ card.name = "J. Smith";
130
+ // non GET methods are mapped onto the instances
131
+ card.$save();
132
+ // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
133
+ // server returns: {id:456, number:'1234', name: 'J. Smith'};
134
+
135
+ // our custom method is mapped as well.
136
+ card.$charge({amount:9.99});
137
+ // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
138
+ });
139
+
140
+ // we can create an instance as well
141
+ var newCard = new CreditCard({number:'0123'});
142
+ newCard.name = "Mike Smith";
143
+ newCard.$save();
144
+ // POST: /user/123/card {number:'0123', name:'Mike Smith'}
145
+ // server returns: {id:789, number:'01234', name: 'Mike Smith'};
146
+ expect(newCard.id).toEqual(789);
147
+ * </pre>
148
+ *
149
+ * The object returned from this function execution is a resource "class" which has "static" method
150
+ * for each action in the definition.
151
+ *
152
+ * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`.
153
+ * When the data is returned from the server then the object is an instance of the resource type and
154
+ * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
155
+ * operations (create, read, update, delete) on server-side data.
156
+
157
+ <pre>
158
+ var User = $resource('/user/:userId', {userId:'@id'});
159
+ var user = User.get({userId:123}, function() {
160
+ user.abc = true;
161
+ user.$save();
162
+ });
163
+ </pre>
164
+ *
165
+ * It's worth noting that the success callback for `get`, `query` and other method gets passed
166
+ * in the response that came from the server as well as $http header getter function, so one
167
+ * could rewrite the above example and get access to http headers as:
168
+ *
169
+ <pre>
170
+ var User = $resource('/user/:userId', {userId:'@id'});
171
+ User.get({userId:123}, function(u, getResponseHeaders){
172
+ u.abc = true;
173
+ u.$save(function(u, putResponseHeaders) {
174
+ //u => saved user object
175
+ //putResponseHeaders => $http header getter
176
+ });
177
+ });
178
+ </pre>
179
+
180
+ * # Buzz client
181
+
182
+ Let's look at what a buzz client created with the `$resource` service looks like:
183
+ <doc:example>
184
+ <doc:source jsfiddle="false">
185
+ <script>
186
+ function BuzzController($resource) {
187
+ this.userId = 'googlebuzz';
188
+ this.Activity = $resource(
189
+ 'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
190
+ {alt:'json', callback:'JSON_CALLBACK'},
191
+ {get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}}
192
+ );
193
+ }
194
+
195
+ BuzzController.prototype = {
196
+ fetch: function() {
197
+ this.activities = this.Activity.get({userId:this.userId});
198
+ },
199
+ expandReplies: function(activity) {
200
+ activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id});
201
+ }
202
+ };
203
+ BuzzController.$inject = ['$resource'];
204
+ </script>
205
+
206
+ <div ng-controller="BuzzController">
207
+ <input ng-model="userId"/>
208
+ <button ng-click="fetch()">fetch</button>
209
+ <hr/>
210
+ <div ng-repeat="item in activities.data.items">
211
+ <h1 style="font-size: 15px;">
212
+ <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
213
+ <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
214
+ <a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
215
+ </h1>
216
+ {{item.object.content | html}}
217
+ <div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;">
218
+ <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
219
+ <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </doc:source>
224
+ <doc:scenario>
225
+ </doc:scenario>
226
+ </doc:example>
227
+ */
228
+ angular.module('ngResource', ['ng']).
229
+ factory('$resource', ['$http', '$parse', function($http, $parse) {
230
+ var DEFAULT_ACTIONS = {
231
+ 'get': {method:'GET'},
232
+ 'save': {method:'POST'},
233
+ 'query': {method:'GET', isArray:true},
234
+ 'remove': {method:'DELETE'},
235
+ 'delete': {method:'DELETE'}
236
+ };
237
+ var noop = angular.noop,
238
+ forEach = angular.forEach,
239
+ extend = angular.extend,
240
+ copy = angular.copy,
241
+ isFunction = angular.isFunction,
242
+ getter = function(obj, path) {
243
+ return $parse(path)(obj);
244
+ };
245
+
246
+ /**
247
+ * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
248
+ * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
249
+ * segments:
250
+ * segment = *pchar
251
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
252
+ * pct-encoded = "%" HEXDIG HEXDIG
253
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
254
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
255
+ * / "*" / "+" / "," / ";" / "="
256
+ */
257
+ function encodeUriSegment(val) {
258
+ return encodeUriQuery(val, true).
259
+ replace(/%26/gi, '&').
260
+ replace(/%3D/gi, '=').
261
+ replace(/%2B/gi, '+');
262
+ }
263
+
264
+
265
+ /**
266
+ * This method is intended for encoding *key* or *value* parts of query component. We need a custom
267
+ * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be
268
+ * encoded per http://tools.ietf.org/html/rfc3986:
269
+ * query = *( pchar / "/" / "?" )
270
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
271
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
272
+ * pct-encoded = "%" HEXDIG HEXDIG
273
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
274
+ * / "*" / "+" / "," / ";" / "="
275
+ */
276
+ function encodeUriQuery(val, pctEncodeSpaces) {
277
+ return encodeURIComponent(val).
278
+ replace(/%40/gi, '@').
279
+ replace(/%3A/gi, ':').
280
+ replace(/%24/g, '$').
281
+ replace(/%2C/gi, ',').
282
+ replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
283
+ }
284
+
285
+ function Route(template, defaults) {
286
+ this.template = template = template + '#';
287
+ this.defaults = defaults || {};
288
+ var urlParams = this.urlParams = {};
289
+ forEach(template.split(/\W/), function(param){
290
+ if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) {
291
+ urlParams[param] = true;
292
+ }
293
+ });
294
+ this.template = template.replace(/\\:/g, ':');
295
+ }
296
+
297
+ Route.prototype = {
298
+ url: function(params) {
299
+ var self = this,
300
+ url = this.template,
301
+ val,
302
+ encodedVal;
303
+
304
+ params = params || {};
305
+ forEach(this.urlParams, function(_, urlParam){
306
+ val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
307
+ if (angular.isDefined(val) && val !== null) {
308
+ encodedVal = encodeUriSegment(val);
309
+ url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1");
310
+ } else {
311
+ url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match,
312
+ leadingSlashes, tail) {
313
+ if (tail.charAt(0) == '/') {
314
+ return tail;
315
+ } else {
316
+ return leadingSlashes + tail;
317
+ }
318
+ });
319
+ }
320
+ });
321
+ url = url.replace(/\/?#$/, '');
322
+ var query = [];
323
+ forEach(params, function(value, key){
324
+ if (!self.urlParams[key]) {
325
+ query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value));
326
+ }
327
+ });
328
+ query.sort();
329
+ url = url.replace(/\/*$/, '');
330
+ return url + (query.length ? '?' + query.join('&') : '');
331
+ }
332
+ };
333
+
334
+
335
+ function ResourceFactory(url, paramDefaults, actions) {
336
+ var route = new Route(url);
337
+
338
+ actions = extend({}, DEFAULT_ACTIONS, actions);
339
+
340
+ function extractParams(data, actionParams){
341
+ var ids = {};
342
+ actionParams = extend({}, paramDefaults, actionParams);
343
+ forEach(actionParams, function(value, key){
344
+ ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
345
+ });
346
+ return ids;
347
+ }
348
+
349
+ function Resource(value){
350
+ copy(value || {}, this);
351
+ }
352
+
353
+ forEach(actions, function(action, name) {
354
+ action.method = angular.uppercase(action.method);
355
+ var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
356
+ Resource[name] = function(a1, a2, a3, a4) {
357
+ var params = {};
358
+ var data;
359
+ var success = noop;
360
+ var error = null;
361
+ switch(arguments.length) {
362
+ case 4:
363
+ error = a4;
364
+ success = a3;
365
+ //fallthrough
366
+ case 3:
367
+ case 2:
368
+ if (isFunction(a2)) {
369
+ if (isFunction(a1)) {
370
+ success = a1;
371
+ error = a2;
372
+ break;
373
+ }
374
+
375
+ success = a2;
376
+ error = a3;
377
+ //fallthrough
378
+ } else {
379
+ params = a1;
380
+ data = a2;
381
+ success = a3;
382
+ break;
383
+ }
384
+ case 1:
385
+ if (isFunction(a1)) success = a1;
386
+ else if (hasBody) data = a1;
387
+ else params = a1;
388
+ break;
389
+ case 0: break;
390
+ default:
391
+ throw "Expected between 0-4 arguments [params, data, success, error], got " +
392
+ arguments.length + " arguments.";
393
+ }
394
+
395
+ var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
396
+ $http({
397
+ method: action.method,
398
+ url: route.url(extend({}, extractParams(data, action.params || {}), params)),
399
+ data: data
400
+ }).then(function(response) {
401
+ var data = response.data;
402
+
403
+ if (data) {
404
+ if (action.isArray) {
405
+ value.length = 0;
406
+ forEach(data, function(item) {
407
+ value.push(new Resource(item));
408
+ });
409
+ } else {
410
+ copy(data, value);
411
+ }
412
+ }
413
+ (success||noop)(value, response.headers);
414
+ }, error);
415
+
416
+ return value;
417
+ };
418
+
419
+
420
+ Resource.prototype['$' + name] = function(a1, a2, a3) {
421
+ var params = extractParams(this),
422
+ success = noop,
423
+ error;
424
+
425
+ switch(arguments.length) {
426
+ case 3: params = a1; success = a2; error = a3; break;
427
+ case 2:
428
+ case 1:
429
+ if (isFunction(a1)) {
430
+ success = a1;
431
+ error = a2;
432
+ } else {
433
+ params = a1;
434
+ success = a2 || noop;
435
+ }
436
+ case 0: break;
437
+ default:
438
+ throw "Expected between 1-3 arguments [params, success, error], got " +
439
+ arguments.length + " arguments.";
440
+ }
441
+ var data = hasBody ? this : undefined;
442
+ Resource[name].call(this, params, data, success, error);
443
+ };
444
+ });
445
+
446
+ Resource.bind = function(additionalParamDefaults){
447
+ return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
448
+ };
449
+
450
+ return Resource;
451
+ }
452
+
453
+ return ResourceFactory;
454
+ }]);
455
+
456
+
457
+ })(window, window.angular);