async-vikunja 1.0.0 → 1.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.
- checksums.yaml +4 -4
- data/README.md +52 -4
- data/lib/async/vikunja/attachments.rb +3 -3
- data/lib/async/vikunja/auth.rb +2 -0
- data/lib/async/vikunja/client.rb +4 -0
- data/lib/async/vikunja/invite_links.rb +69 -0
- data/lib/async/vikunja/mcp.rb +25 -0
- data/lib/async/vikunja/migration.rb +6 -0
- data/lib/async/vikunja/projects.rb +1 -1
- data/lib/async/vikunja/representation.rb +20 -6
- data/lib/async/vikunja/service.rb +1 -1
- data/lib/async/vikunja/settings.rb +5 -1
- data/lib/async/vikunja/submit.rb +12 -0
- data/lib/async/vikunja/tasks.rb +2 -2
- data/lib/async/vikunja/users.rb +2 -2
- data/lib/async/vikunja/version.rb +1 -1
- data/lib/async/vikunja/wrapper.rb +110 -10
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 542a774d84fa94455ddbe6fad372b4af80be46ba16d792598efcd921af1a5b8d
|
|
4
|
+
data.tar.gz: c88c89e32e48806172a5329c4684b224bc7ef7f76636167bff3c58eededc1122
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1f4eff3b457265fef50003cb8e98be725a219648bb5b2f0a268288e1e17177ed129a7c573dd9682114dc1ee8b2ede58c44219da785cdcec20008cd8a8a2b47a
|
|
7
|
+
data.tar.gz: d14784601ab95bef94f217bbba4a86b83ec97fd4cacfd811573082e746d1dfc4e27f59bc2467410fdefffe2f3fd06621ff3264c09a075024cce5967a54f7ff49
|
data/README.md
CHANGED
|
@@ -175,21 +175,69 @@ vikunja.project(1).search_users("test").map(&:username)
|
|
|
175
175
|
vikunja.project(1).remove_user("tester")
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
Bytes go up as they are, so read a file in binary and say what it is. Several parts may share
|
|
179
|
+
one field name.
|
|
180
|
+
|
|
178
181
|
```ruby
|
|
179
182
|
result = vikunja.task(7).upload_attachments([
|
|
180
|
-
{name: "files", filename: "notes.txt", content_type: "text/plain", body: File.
|
|
183
|
+
{name: "files", filename: "notes.txt", content_type: "text/plain", body: File.binread("notes.txt")},
|
|
184
|
+
{name: "files", filename: "diagram.png", content_type: "image/png", body: File.binread("diagram.png")},
|
|
181
185
|
])
|
|
182
186
|
|
|
183
187
|
result.success.map{|attachment| attachment.file.name}
|
|
184
188
|
result.errors.map(&:message)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
An attachment knows its own address wherever it arrived, so it downloads and deletes itself.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
stored = result.success.first
|
|
195
|
+
|
|
196
|
+
stored.download.bytes
|
|
197
|
+
stored.delete
|
|
185
198
|
|
|
186
199
|
vikunja.task(7).task_attachments.map{|attachment| attachment.file.size}
|
|
187
|
-
vikunja.task(7).
|
|
200
|
+
vikunja.task(7).attachments.map{|attachment| attachment.download.bytes.bytesize}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A download is the bytes the API answered with, not a document. It is fetched when it is first
|
|
204
|
+
read, and the headers arrive with it.
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
file = vikunja.task(7).download_attachment(3)
|
|
208
|
+
|
|
209
|
+
file.media_type
|
|
210
|
+
file.bytes
|
|
211
|
+
|
|
212
|
+
vikunja.task(7).attachment(3).preview("sm").bytes
|
|
188
213
|
vikunja.task(7).attachment(3).delete
|
|
189
214
|
```
|
|
190
215
|
|
|
216
|
+
A file is as large as it is. Give a download a block and the body arrives a chunk at a time, so
|
|
217
|
+
nothing holds the whole of it:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
File.open("notes.txt", "wb") do |out|
|
|
221
|
+
vikunja.task(7).download_attachment(3) do |response|
|
|
222
|
+
response.body.each{|chunk| out.write(chunk)}
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Every download takes a block the same way: `avatar`, `background`, `preview`, `totp_qrcode`,
|
|
228
|
+
`notifications_atom` and `download_export`.
|
|
229
|
+
|
|
230
|
+
An upload streams the other way. Hand a part an open file rather than its bytes, and it is read
|
|
231
|
+
from disk as it goes on the wire:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
vikunja.task(7).upload_attachments([
|
|
235
|
+
{name: "files", filename: "recording.mp4", content_type: "video/mp4", body: File.open("recording.mp4", "rb")},
|
|
236
|
+
])
|
|
237
|
+
```
|
|
238
|
+
|
|
191
239
|
```ruby
|
|
192
|
-
vikunja.project(1).upload_background({name: "background", filename: "wall.jpg", content_type: "image/jpeg", body: File.
|
|
240
|
+
vikunja.project(1).upload_background({name: "background", filename: "wall.jpg", content_type: "image/jpeg", body: File.binread("wall.jpg")})
|
|
193
241
|
|
|
194
242
|
background = vikunja.project(1).background
|
|
195
243
|
|
|
@@ -248,7 +296,7 @@ vikunja.create_caldav_token.token
|
|
|
248
296
|
```ruby
|
|
249
297
|
vikunja.user.settings.timezone
|
|
250
298
|
vikunja.update_settings({name: "Tester", week_start: 1, timezone: "Europe/London"}).message
|
|
251
|
-
vikunja.upload_avatar({name: "avatar", filename: "me.png", content_type: "image/png", body: File.
|
|
299
|
+
vikunja.upload_avatar({name: "avatar", filename: "me.png", content_type: "image/png", body: File.binread("me.png")})
|
|
252
300
|
vikunja.avatar("tester", size: 64).bytes
|
|
253
301
|
vikunja.set_avatar_provider(avatar_provider: "initials")
|
|
254
302
|
|
|
@@ -25,9 +25,9 @@ module Async
|
|
|
25
25
|
|
|
26
26
|
def file = self.nested(File, self.value[:file])
|
|
27
27
|
|
|
28
|
-
def download = Download.
|
|
28
|
+
def download(&block) = Download.open(@resource, &block)
|
|
29
29
|
|
|
30
|
-
def preview(size) = Download.
|
|
30
|
+
def preview(size, &block) = Download.open(@resource.with(parameters: {preview_size: size}), &block)
|
|
31
31
|
|
|
32
32
|
def delete = self.discard(@resource)
|
|
33
33
|
end
|
|
@@ -39,7 +39,7 @@ module Async
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
class AttachmentUploadResult < Representation
|
|
42
|
-
def success = self.nested_list(TaskAttachment, self.value[:success])
|
|
42
|
+
def success = self.nested_list(TaskAttachment, self.value[:success], identify: true)
|
|
43
43
|
|
|
44
44
|
def errors = self.nested_list(AttachmentUploadError, self.value[:errors])
|
|
45
45
|
end
|
data/lib/async/vikunja/auth.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Async
|
|
|
38
38
|
|
|
39
39
|
def refresh_token = self.submit(AuthToken, self.with(path: "user/token/refresh"), nil)
|
|
40
40
|
|
|
41
|
+
def register(payload) = self.submit(User, self.with(path: "register"), payload, identify: nil)
|
|
42
|
+
|
|
41
43
|
def confirm_email(payload) = self.submit(Message, self.with(path: "user/confirm"), payload)
|
|
42
44
|
|
|
43
45
|
def reset_password(payload) = self.submit(Message, self.with(path: "user/password/reset"), payload)
|
data/lib/async/vikunja/client.rb
CHANGED
|
@@ -9,7 +9,9 @@ require_relative "buckets"
|
|
|
9
9
|
require_relative "comments"
|
|
10
10
|
require_relative "error"
|
|
11
11
|
require_relative "filters"
|
|
12
|
+
require_relative "invite_links"
|
|
12
13
|
require_relative "labels"
|
|
14
|
+
require_relative "mcp"
|
|
13
15
|
require_relative "migration"
|
|
14
16
|
require_relative "notifications"
|
|
15
17
|
require_relative "page"
|
|
@@ -43,7 +45,9 @@ module Async
|
|
|
43
45
|
include Auth
|
|
44
46
|
include Bots
|
|
45
47
|
include Filters
|
|
48
|
+
include InviteLinks
|
|
46
49
|
include Labels
|
|
50
|
+
include MCP
|
|
47
51
|
include Migration
|
|
48
52
|
include Notifications
|
|
49
53
|
include Projects
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class InviteLinkTeam < Representation
|
|
6
|
+
def id = self.value[:id]
|
|
7
|
+
|
|
8
|
+
def name = self.value[:name]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class UserInviteLink < Representation
|
|
12
|
+
def id = self.value[:id]
|
|
13
|
+
|
|
14
|
+
def name = self.value[:name]
|
|
15
|
+
|
|
16
|
+
def token = self.value[:token]
|
|
17
|
+
|
|
18
|
+
def max_uses = self.value[:max_uses]
|
|
19
|
+
|
|
20
|
+
def uses = self.value[:uses]
|
|
21
|
+
|
|
22
|
+
def expires_at = self.value[:expires_at]
|
|
23
|
+
|
|
24
|
+
def skip_email_confirm = self.value[:skip_email_confirm]
|
|
25
|
+
|
|
26
|
+
def created = self.value[:created]
|
|
27
|
+
|
|
28
|
+
def updated = self.value[:updated]
|
|
29
|
+
|
|
30
|
+
def created_by = self.nested(User, self.value[:created_by])
|
|
31
|
+
|
|
32
|
+
def teams = self.nested_list(InviteLinkTeam, self.value[:teams])
|
|
33
|
+
|
|
34
|
+
def spent? = self.max_uses.to_i.positive? && self.uses.to_i >= self.max_uses.to_i
|
|
35
|
+
|
|
36
|
+
def delete = self.discard(@resource)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class PublicInviteLink < Representation
|
|
40
|
+
def name = self.value[:name]
|
|
41
|
+
|
|
42
|
+
def skip_email_confirm = self.value[:skip_email_confirm]
|
|
43
|
+
|
|
44
|
+
def teams = self.nested_list(InviteLinkTeam, self.value[:teams])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class UserInviteLinkPage < Page
|
|
48
|
+
MEMBER = UserInviteLink
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class InviteLinkTeamPage < Page
|
|
52
|
+
MEMBER = InviteLinkTeam
|
|
53
|
+
|
|
54
|
+
def member_resource(record) = @resource
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module InviteLinks
|
|
58
|
+
def invite_links(**parameters) = Collection.for(self, "admin/invite-links", UserInviteLinkPage, **parameters)
|
|
59
|
+
|
|
60
|
+
def create_invite_link(payload) = self.submit(UserInviteLink, self.with(path: "admin/invite-links"), payload, identify: true)
|
|
61
|
+
|
|
62
|
+
def remove_invite_link(id) = self.discard(self.with(path: "admin/invite-links/" + Vikunja.escape(id)))
|
|
63
|
+
|
|
64
|
+
def invite_link_teams(**parameters) = Collection.for(self, "admin/teams", InviteLinkTeamPage, **parameters)
|
|
65
|
+
|
|
66
|
+
def check_invite_link(token) = self.submit(PublicInviteLink, self.with(path: "invite-links/check"), {token: token})
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class TokenPresets < Representation
|
|
6
|
+
def full = self.value[:full]
|
|
7
|
+
|
|
8
|
+
def read_only = self.value[:read_only]
|
|
9
|
+
|
|
10
|
+
def typed = self.value[:typed]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class ConnectionSettings < Representation
|
|
14
|
+
def endpoint = self.value[:endpoint]
|
|
15
|
+
|
|
16
|
+
def routes = self.value[:routes]
|
|
17
|
+
|
|
18
|
+
def presets = self.nested(TokenPresets, self.value[:presets])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module MCP
|
|
22
|
+
def mcp_info = ConnectionSettings.new(self.with(path: "mcp/info"))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -58,6 +58,12 @@ module Async
|
|
|
58
58
|
def started_at = self.value[:started_at]
|
|
59
59
|
|
|
60
60
|
def finished_at = self.value[:finished_at]
|
|
61
|
+
|
|
62
|
+
def error_kind = self.value[:error_kind]
|
|
63
|
+
|
|
64
|
+
def error_message = self.value[:error_message]
|
|
65
|
+
|
|
66
|
+
def failed? = !self.error_kind.nil?
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
module Migration
|
|
@@ -61,7 +61,7 @@ module Async
|
|
|
61
61
|
|
|
62
62
|
def task_by_index(index) = TaskReadOneBody.new(@resource.with(path: "tasks/by-index/" + Vikunja.escape(index)))
|
|
63
63
|
|
|
64
|
-
def background = Download.
|
|
64
|
+
def background(&block) = Download.open(@resource.with(path: "background"), &block)
|
|
65
65
|
|
|
66
66
|
def search_users(query) = UserSearchPage.new(@resource.with(path: "users/search", parameters: {q: query}))
|
|
67
67
|
|
|
@@ -41,15 +41,15 @@ module Async
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def nested(klass, value)
|
|
44
|
+
def nested(klass, value, identify: nil)
|
|
45
45
|
if value
|
|
46
|
-
klass.new(@resource, value: value, metadata: self.metadata)
|
|
46
|
+
klass.new(self.identified(@resource, identify, value), value: value, metadata: self.metadata)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def nested_list(klass, values)
|
|
50
|
+
def nested_list(klass, values, identify: nil)
|
|
51
51
|
Array(values).map do |value|
|
|
52
|
-
self.nested(klass, value)
|
|
52
|
+
self.nested(klass, value, identify: identify)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -63,11 +63,25 @@ module Async
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
class Download < Representation
|
|
66
|
-
WRAPPER =
|
|
66
|
+
WRAPPER = Transfer.new
|
|
67
|
+
|
|
68
|
+
def self.open(resource, &block)
|
|
69
|
+
if block
|
|
70
|
+
self.get(resource) do |_resource, response, _klass|
|
|
71
|
+
block.call(response)
|
|
72
|
+
end
|
|
73
|
+
else
|
|
74
|
+
self.new(resource)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
67
77
|
|
|
68
78
|
def bytes = self.value
|
|
69
79
|
|
|
70
|
-
def media_type
|
|
80
|
+
def media_type
|
|
81
|
+
self.bytes
|
|
82
|
+
|
|
83
|
+
self.metadata["content-type"]
|
|
84
|
+
end
|
|
71
85
|
end
|
|
72
86
|
end
|
|
73
87
|
end
|
|
@@ -131,7 +131,7 @@ module Async
|
|
|
131
131
|
|
|
132
132
|
def routes = Representation.new(self.with(path: "routes"))
|
|
133
133
|
|
|
134
|
-
def notifications_atom = Download.
|
|
134
|
+
def notifications_atom(&block) = Download.open(self.with(path: "notifications.atom"), &block)
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
@@ -43,8 +43,12 @@ module Async
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
module Settings
|
|
46
|
+
def settings = UserGeneralSettings.new(self.with(path: "user/settings/general"))
|
|
47
|
+
|
|
46
48
|
def update_settings(payload) = self.submit(UserActionMessageBody, self.with(path: "user/settings/general"), payload, method: :put)
|
|
47
49
|
|
|
50
|
+
def patch_settings(payload) = self.merge_patch(UserActionMessageBody, self.with(path: "user/settings/general"), payload)
|
|
51
|
+
|
|
48
52
|
def avatar_provider = UserAvatarProviderBody.new(self.with(path: "user/settings/avatar/provider"))
|
|
49
53
|
|
|
50
54
|
def set_avatar_provider(avatar_provider:) = self.submit(UserAvatarProviderBody, self.with(path: "user/settings/avatar/provider"), {avatar_provider: avatar_provider}, method: :put)
|
|
@@ -61,7 +65,7 @@ module Async
|
|
|
61
65
|
|
|
62
66
|
def disable_totp(password:) = self.submit(Message, self.with(path: "user/settings/totp/disable"), {password: password})
|
|
63
67
|
|
|
64
|
-
def totp_qrcode = Download.
|
|
68
|
+
def totp_qrcode(&block) = Download.open(self.with(path: "user/settings/totp/qrcode"), &block)
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
end
|
data/lib/async/vikunja/submit.rb
CHANGED
|
@@ -53,6 +53,18 @@ module Async
|
|
|
53
53
|
self.submit(klass, resource, parts, method: method, through: UploadRepresentation)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def fetch(klass, resource, payload = nil, method: :post, &block)
|
|
57
|
+
if block
|
|
58
|
+
self.with_retry do
|
|
59
|
+
klass.public_send(method, resource, payload) do |_target, response|
|
|
60
|
+
block.call(response)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
else
|
|
64
|
+
self.submit(klass, resource, payload, method: method)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
56
68
|
def discard(resource, payload = nil, method: :delete)
|
|
57
69
|
self.with_retry do
|
|
58
70
|
Representation.public_send(method, resource, payload) do |target, response|
|
data/lib/async/vikunja/tasks.rb
CHANGED
|
@@ -65,7 +65,7 @@ module Async
|
|
|
65
65
|
|
|
66
66
|
def assignees = self.nested_list(User, self.value[:assignees])
|
|
67
67
|
|
|
68
|
-
def attachments = self.nested_list(TaskAttachment, self.value[:attachments])
|
|
68
|
+
def attachments = self.nested_list(TaskAttachment, self.value[:attachments], identify: "attachments/")
|
|
69
69
|
|
|
70
70
|
def buckets = self.nested_list(Bucket, self.value[:buckets])
|
|
71
71
|
|
|
@@ -97,7 +97,7 @@ module Async
|
|
|
97
97
|
|
|
98
98
|
def attachment(id) = TaskAttachment.new(@resource.with(path: "attachments/" + Vikunja.escape(id)))
|
|
99
99
|
|
|
100
|
-
def download_attachment(id) = Download.
|
|
100
|
+
def download_attachment(id, &block) = Download.open(@resource.with(path: "attachments/" + Vikunja.escape(id)), &block)
|
|
101
101
|
|
|
102
102
|
def create_comment(payload) = self.submit(TaskComment, @resource.with(path: "comments"), payload, identify: true)
|
|
103
103
|
|
data/lib/async/vikunja/users.rb
CHANGED
|
@@ -79,7 +79,7 @@ module Async
|
|
|
79
79
|
|
|
80
80
|
def users(query) = UserSearchPage.new(self.with(path: "users", parameters: {q: query}))
|
|
81
81
|
|
|
82
|
-
def avatar(username, **parameters) = Download.
|
|
82
|
+
def avatar(username, **parameters, &block) = Download.open(self.with(path: "avatar/" + Vikunja.escape(username), parameters: parameters), &block)
|
|
83
83
|
|
|
84
84
|
def timezones = Representation.new(self.with(path: "user/timezones"))
|
|
85
85
|
|
|
@@ -95,7 +95,7 @@ module Async
|
|
|
95
95
|
|
|
96
96
|
def request_export(password:) = self.submit(Message, self.with(path: "user/export/request"), {password: password})
|
|
97
97
|
|
|
98
|
-
def download_export(password
|
|
98
|
+
def download_export(password:, &block) = self.fetch(Download, self.with(path: "user/export/download"), {password: password}, &block)
|
|
99
99
|
|
|
100
100
|
def request_deletion(password:) = self.discard(self.with(path: "user/deletion/request"), {password: password}, method: :post)
|
|
101
101
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
require "async/rest/wrapper/json"
|
|
2
2
|
require "protocol/http/body/buffered"
|
|
3
|
+
require "protocol/http/body/file"
|
|
4
|
+
require "protocol/http/body/readable"
|
|
5
|
+
|
|
6
|
+
require_relative "error"
|
|
3
7
|
|
|
4
8
|
module Async
|
|
5
9
|
module Vikunja
|
|
@@ -30,6 +34,29 @@ module Async
|
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
class Transfer < Wrapper
|
|
38
|
+
ANY = "*/*"
|
|
39
|
+
|
|
40
|
+
def prepare_request(request, payload)
|
|
41
|
+
request.headers["accept"] ||= ANY
|
|
42
|
+
|
|
43
|
+
if payload
|
|
44
|
+
request.headers["content-type"] = self.content_type
|
|
45
|
+
request.body = ::Protocol::HTTP::Body::Buffered.new([::JSON.dump(payload)])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def process_response(request, response)
|
|
50
|
+
super
|
|
51
|
+
|
|
52
|
+
unless response.success?
|
|
53
|
+
raise Failure.for(response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
response
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
33
60
|
class Multipart < Wrapper
|
|
34
61
|
MULTIPART_FORM_DATA = "multipart/form-data"
|
|
35
62
|
|
|
@@ -39,30 +66,103 @@ module Async
|
|
|
39
66
|
|
|
40
67
|
CRLF = "\r\n"
|
|
41
68
|
|
|
69
|
+
class Body < ::Protocol::HTTP::Body::Readable
|
|
70
|
+
def initialize(bodies)
|
|
71
|
+
@bodies = bodies
|
|
72
|
+
@index = 0
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def length
|
|
76
|
+
lengths = @bodies.map(&:length)
|
|
77
|
+
|
|
78
|
+
if lengths.include?(nil)
|
|
79
|
+
nil
|
|
80
|
+
else
|
|
81
|
+
lengths.sum
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def empty? = @index >= @bodies.size
|
|
86
|
+
|
|
87
|
+
def ready? = true
|
|
88
|
+
|
|
89
|
+
def read
|
|
90
|
+
chunk = nil
|
|
91
|
+
|
|
92
|
+
while chunk.nil? && @index < @bodies.size
|
|
93
|
+
chunk = @bodies[@index].read
|
|
94
|
+
|
|
95
|
+
if chunk.nil?
|
|
96
|
+
@index += 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
chunk
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def close(error = nil)
|
|
104
|
+
@bodies.each do |body|
|
|
105
|
+
body.close(error)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
@index = @bodies.size
|
|
109
|
+
|
|
110
|
+
super
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
42
114
|
def prepare_request(request, payload)
|
|
43
115
|
request.headers["accept"] ||= self.content_type
|
|
44
116
|
|
|
45
117
|
if payload
|
|
46
118
|
request.headers["content-type"] = MULTIPART_FORM_DATA + "; boundary=" + BOUNDARY
|
|
47
|
-
request.body =
|
|
119
|
+
request.body = Body.new(self.bodies_for(payload))
|
|
48
120
|
end
|
|
49
121
|
end
|
|
50
122
|
|
|
51
|
-
def
|
|
52
|
-
|
|
123
|
+
def bodies_for(parts)
|
|
124
|
+
[].tap do |bodies|
|
|
53
125
|
Array(parts).each do |part|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
encoded << CRLF
|
|
58
|
-
encoded << part.fetch(:body).to_s
|
|
59
|
-
encoded << CRLF
|
|
126
|
+
bodies << self.buffered(self.preamble_of(part))
|
|
127
|
+
bodies << self.body_of(part)
|
|
128
|
+
bodies << self.buffered(CRLF)
|
|
60
129
|
end
|
|
61
130
|
|
|
62
|
-
|
|
131
|
+
bodies << self.buffered("--" + BOUNDARY + "--" + CRLF)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def buffered(text) = ::Protocol::HTTP::Body::Buffered.new([self.binary(text)])
|
|
136
|
+
|
|
137
|
+
def binary(text)
|
|
138
|
+
case text.encoding
|
|
139
|
+
when ::Encoding::BINARY
|
|
140
|
+
text
|
|
141
|
+
else
|
|
142
|
+
text.b
|
|
63
143
|
end
|
|
64
144
|
end
|
|
65
145
|
|
|
146
|
+
def body_of(part)
|
|
147
|
+
body = part.fetch(:body)
|
|
148
|
+
|
|
149
|
+
case body
|
|
150
|
+
when ::Protocol::HTTP::Body::Readable
|
|
151
|
+
body
|
|
152
|
+
when ::IO, ::StringIO
|
|
153
|
+
::Protocol::HTTP::Body::File.new(body)
|
|
154
|
+
else
|
|
155
|
+
self.buffered(body.to_s)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def preamble_of(part)
|
|
160
|
+
"--" + BOUNDARY + CRLF +
|
|
161
|
+
"content-disposition: form-data; " + self.disposition_of(part) + CRLF +
|
|
162
|
+
"content-type: " + (part[:content_type] || OCTET_STREAM) + CRLF +
|
|
163
|
+
CRLF
|
|
164
|
+
end
|
|
165
|
+
|
|
66
166
|
def disposition_of(part)
|
|
67
167
|
("name=" + part.fetch(:name).to_s.inspect).tap do |disposition|
|
|
68
168
|
if part[:filename]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-vikunja
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Kidd
|
|
@@ -56,7 +56,9 @@ files:
|
|
|
56
56
|
- lib/async/vikunja/error.rb
|
|
57
57
|
- lib/async/vikunja/escape.rb
|
|
58
58
|
- lib/async/vikunja/filters.rb
|
|
59
|
+
- lib/async/vikunja/invite_links.rb
|
|
59
60
|
- lib/async/vikunja/labels.rb
|
|
61
|
+
- lib/async/vikunja/mcp.rb
|
|
60
62
|
- lib/async/vikunja/migration.rb
|
|
61
63
|
- lib/async/vikunja/notifications.rb
|
|
62
64
|
- lib/async/vikunja/page.rb
|