yourimageshare 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 +28 -1
- data/lib/yourimageshare/client.rb +103 -17
- data/lib/yourimageshare/types.rb +22 -6
- data/lib/yourimageshare/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b5d4188f0b04cf69a8275bc809330e30bd0e5d9b7451a81b5080c5db3b26696
|
|
4
|
+
data.tar.gz: 3c03786f86644591965adce714cb982b6c3ba5b354041c4b1275704e88d8ffa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8befda17710e2812ae563cfc8c903566975dee700a5186b7d2688b0166bed3253aae8b477c05d3d54e67e3f345c9d81a0efda88be8223b5295e167748c36e857
|
|
7
|
+
data.tar.gz: eb6ac978baf41b1d186213862490160ba005a2dc5f2eaad5fdb812e6e60c1f8e09328c4a2530abd545f2799ff1945e8635b6ccbc584457b52671767e386de8f4
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ upload API. Zero gem dependencies (standard library only), Ruby 2.7+.
|
|
|
10
10
|
[yourimageshare.com/my-account](https://yourimageshare.com/my-account).
|
|
11
11
|
- **Full HTTP reference:** [yourimageshare.com/about/api](https://yourimageshare.com/about/api)
|
|
12
12
|
or [API.md in this repo](../API.md).
|
|
13
|
-
- Same API, same result shapes, in [JavaScript/TypeScript](https://www.npmjs.com/package/yourimageshare), [Python](https://pypi.org/project/yourimageshare/), [PHP](https://packagist.org/packages/yourimageshare/yourimageshare-php), and [Go](https://pkg.go.dev/github.com/
|
|
13
|
+
- Same API, same result shapes, in [JavaScript/TypeScript](https://www.npmjs.com/package/yourimageshare), [Python](https://pypi.org/project/yourimageshare/), [PHP](https://packagist.org/packages/yourimageshare/yourimageshare-php), and [Go](https://pkg.go.dev/github.com/yourimageshare/yourimageshare/go) too.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -49,6 +49,33 @@ listing.data.each { |item| puts "#{item.id} #{item.direct}" }
|
|
|
49
49
|
client.delete(result.id)
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Large files, duplicates, links and thumbnails
|
|
53
|
+
|
|
54
|
+
Files up to 200 MB are supported. Anything over 90 MB is sent in 5 MB pieces automatically (one request can carry at most 100 MB). If your account already uploaded the exact same file, the existing upload is returned with `duplicate` set - opt out with the allow-duplicate option. Results also carry `thumb` (a 280 px WebP thumbnail), `width`, `height`, `size` and `locked`. Store `src`, not `path`: `path` can change shortly after upload when the file is converted (WebP/MP4).
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
result = client.upload("video.mp4") { |sent, total| puts "#{sent * 100 / total}%" }
|
|
58
|
+
puts result.src, result.thumb, result.width, result.duplicate
|
|
59
|
+
|
|
60
|
+
# a fresh copy even if this exact file is already on your account
|
|
61
|
+
client.upload("photo.jpg", allow_duplicate: true)
|
|
62
|
+
|
|
63
|
+
# let the server download a public link (up to 200 MB)
|
|
64
|
+
client.upload_url("https://example.com/photo.jpg")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Visibility, titles and changing an upload
|
|
68
|
+
|
|
69
|
+
Each upload has a `visibility`: `unlisted` (the default - file and page work for anyone with the link, not listed anywhere), `private` (file only - the page link sends everyone but you to the file) or `public` (listed on the site). Uploads can carry a `title` (90 characters) and `description` (500). New uploads return a one-time `delete_url` that deletes the upload without an API key. `get`/`update` need the full API key.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
result = client.upload("photo.jpg", visibility: "private", title: "Sunset")
|
|
73
|
+
puts result.delete_url # shown once - keep it if you need it
|
|
74
|
+
|
|
75
|
+
one = client.get(result.id)
|
|
76
|
+
client.update(result.id, visibility: "public", description: "Lake at dusk")
|
|
77
|
+
```
|
|
78
|
+
|
|
52
79
|
### Error handling
|
|
53
80
|
|
|
54
81
|
Unlike the Go SDK's `(result, error)` return style, the Ruby SDK follows
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
require "net/http"
|
|
2
2
|
require "uri"
|
|
3
3
|
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require "stringio"
|
|
4
6
|
|
|
5
7
|
module YourImageShare
|
|
6
8
|
DEFAULT_BASE_URL = "https://yourimageshare.com/api"
|
|
9
|
+
# Files above this size are sent in pieces (one request can carry at most 100 MB).
|
|
10
|
+
CHUNK_THRESHOLD = 90 * 1024 * 1024
|
|
11
|
+
# Size of one piece of a chunked upload (the API accepts at most 5 MB).
|
|
12
|
+
CHUNK_SIZE = 5 * 1024 * 1024
|
|
7
13
|
|
|
8
14
|
# Talks to the YourImageShare upload API. Create one with
|
|
9
15
|
# YourImageShare::Client.new(api_key). Get a key from the API tab at
|
|
@@ -17,30 +23,60 @@ module YourImageShare
|
|
|
17
23
|
@timeout = timeout
|
|
18
24
|
end
|
|
19
25
|
|
|
20
|
-
# Uploads a local file by path. expires_in (seconds, 60 to
|
|
21
|
-
# 30 days) auto-deletes the upload later; nil means a
|
|
22
|
-
|
|
26
|
+
# Uploads a local file by path (up to 200 MB). expires_in (seconds, 60 to
|
|
27
|
+
# 2,592,000 = 30 days) auto-deletes the upload later; nil means a
|
|
28
|
+
# permanent upload. If your account already uploaded this exact file,
|
|
29
|
+
# that upload is returned with duplicate = true - pass
|
|
30
|
+
# allow_duplicate: true to store a new copy. Files over 90 MB are sent in
|
|
31
|
+
# 5 MB pieces automatically; the block, if given, is called with
|
|
32
|
+
# (bytes_sent, total) after each piece.
|
|
33
|
+
def upload(file_path, expires_in: nil, allow_duplicate: false, visibility: nil, title: nil, description: nil, &on_progress)
|
|
23
34
|
File.open(file_path, "rb") do |f|
|
|
24
|
-
upload_io(f, File.basename(file_path), expires_in: expires_in
|
|
35
|
+
upload_io(f, File.basename(file_path), expires_in: expires_in, allow_duplicate: allow_duplicate,
|
|
36
|
+
visibility: visibility, title: title, description: description, &on_progress)
|
|
25
37
|
end
|
|
26
38
|
end
|
|
27
39
|
|
|
28
40
|
# Uploads from any IO-like object (must respond to #read). filename
|
|
29
41
|
# should include a real extension so the server can infer content type.
|
|
30
|
-
def upload_io(io, filename, expires_in: nil)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
def upload_io(io, filename, expires_in: nil, allow_duplicate: false, visibility: nil, title: nil, description: nil, &on_progress)
|
|
43
|
+
size = io_size(io)
|
|
44
|
+
if size && size > CHUNK_THRESHOLD
|
|
45
|
+
upload_id = send_chunks(io, size, &on_progress)
|
|
46
|
+
return post_upload([["upload_id", upload_id], ["filename", filename]], expires_in, allow_duplicate,
|
|
47
|
+
visibility, title, description)
|
|
48
|
+
end
|
|
34
49
|
|
|
35
|
-
form = [["uploads", io, { filename: filename }]]
|
|
36
|
-
form << ["expires_in", expires_in.to_s] if expires_in && expires_in > 0
|
|
37
50
|
# Net::HTTP streams IO form values in chunks rather than buffering
|
|
38
|
-
# the whole file into memory
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
# the whole file into memory.
|
|
52
|
+
post_upload([["uploads", io, { filename: filename }]], expires_in, allow_duplicate, visibility, title, description)
|
|
53
|
+
end
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
# Uploads from a public http(s) link: the server downloads the file
|
|
56
|
+
# itself (up to 200 MB).
|
|
57
|
+
def upload_url(url, expires_in: nil, allow_duplicate: false, visibility: nil, title: nil, description: nil)
|
|
58
|
+
post_upload([["url", url]], expires_in, allow_duplicate, visibility, title, description)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# One of your uploads (needs the full API key).
|
|
62
|
+
def get(id)
|
|
63
|
+
uri = URI("#{@base_url.chomp("/")}/#{URI.encode_www_form_component(id)}")
|
|
64
|
+
request = Net::HTTP::Get.new(uri)
|
|
65
|
+
set_common_headers(request)
|
|
66
|
+
ListedUpload.from_json(execute(uri, request)["data"] || {})
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Changes the visibility ("private", "unlisted", "public"), title or
|
|
70
|
+
# description of one of your uploads (needs the full API key). Only the
|
|
71
|
+
# arguments you pass change; an empty string clears a title/description.
|
|
72
|
+
def update(id, visibility: nil, title: nil, description: nil)
|
|
73
|
+
changes = { visibility: visibility, title: title, description: description }.compact
|
|
74
|
+
uri = URI("#{@base_url.chomp("/")}/#{URI.encode_www_form_component(id)}")
|
|
75
|
+
request = Net::HTTP::Patch.new(uri)
|
|
76
|
+
set_common_headers(request)
|
|
77
|
+
request["Content-Type"] = "application/json"
|
|
78
|
+
request.body = JSON.generate(changes)
|
|
79
|
+
ListedUpload.from_json(execute(uri, request)["data"] || {})
|
|
44
80
|
end
|
|
45
81
|
|
|
46
82
|
# Returns your uploads, newest first, 50 per page. page < 2 fetches the
|
|
@@ -67,14 +103,64 @@ module YourImageShare
|
|
|
67
103
|
|
|
68
104
|
private
|
|
69
105
|
|
|
106
|
+
def post_upload(form, expires_in, allow_duplicate, visibility = nil, title = nil, description = nil)
|
|
107
|
+
uri = URI(@base_url)
|
|
108
|
+
request = Net::HTTP::Post.new(uri)
|
|
109
|
+
set_common_headers(request)
|
|
110
|
+
form << ["expires_in", expires_in.to_s] if expires_in && expires_in > 0
|
|
111
|
+
form << ["allow_duplicate", "1"] if allow_duplicate
|
|
112
|
+
form << ["visibility", visibility.to_s] if visibility
|
|
113
|
+
form << ["title", title] unless title.nil?
|
|
114
|
+
form << ["description", description] unless description.nil?
|
|
115
|
+
request.set_form(form, "multipart/form-data")
|
|
116
|
+
|
|
117
|
+
body = execute(uri, request, [@timeout, 180].max)
|
|
118
|
+
UploadResult.from_json(body["data"] || {})
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def send_chunks(io, size)
|
|
122
|
+
upload_id = SecureRandom.hex(16)
|
|
123
|
+
total = (size + CHUNK_SIZE - 1) / CHUNK_SIZE
|
|
124
|
+
sent = 0
|
|
125
|
+
uri = URI("#{@base_url.chomp("/")}/chunk")
|
|
126
|
+
total.times do |index|
|
|
127
|
+
piece = io.read(CHUNK_SIZE)
|
|
128
|
+
attempt = 0
|
|
129
|
+
begin
|
|
130
|
+
attempt += 1
|
|
131
|
+
request = Net::HTTP::Post.new(uri)
|
|
132
|
+
set_common_headers(request)
|
|
133
|
+
request.set_form([["upload_id", upload_id], ["index", index.to_s], ["total", total.to_s],
|
|
134
|
+
["chunk", StringIO.new(piece), { filename: "piece" }]], "multipart/form-data")
|
|
135
|
+
execute(uri, request)
|
|
136
|
+
rescue APIError, IOError, SystemCallError, Timeout::Error => e
|
|
137
|
+
# network errors and 5xx are retried; anything the server refused (4xx) is final
|
|
138
|
+
status = e.respond_to?(:status) ? e.status.to_i : 0
|
|
139
|
+
retry if attempt < 3 && !(status >= 400 && status < 500)
|
|
140
|
+
raise
|
|
141
|
+
end
|
|
142
|
+
sent += piece.bytesize
|
|
143
|
+
yield(sent, size) if block_given?
|
|
144
|
+
end
|
|
145
|
+
upload_id
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def io_size(io)
|
|
149
|
+
return io.size - io.pos if io.respond_to?(:size) && io.respond_to?(:pos)
|
|
150
|
+
return File.size(io.path) - io.pos if io.respond_to?(:path) && io.respond_to?(:pos)
|
|
151
|
+
nil
|
|
152
|
+
rescue StandardError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
70
156
|
def set_common_headers(request)
|
|
71
157
|
request["X-API-Key"] = @api_key
|
|
72
158
|
request["User-Agent"] = "yourimageshare-ruby/#{VERSION}"
|
|
73
159
|
end
|
|
74
160
|
|
|
75
|
-
def execute(uri, request)
|
|
161
|
+
def execute(uri, request, read_timeout = @timeout)
|
|
76
162
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
77
|
-
open_timeout: @timeout, read_timeout:
|
|
163
|
+
open_timeout: @timeout, read_timeout: read_timeout, write_timeout: read_timeout) do |http|
|
|
78
164
|
http.request(request)
|
|
79
165
|
end
|
|
80
166
|
|
data/lib/yourimageshare/types.rb
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
module YourImageShare
|
|
2
2
|
# Response shape for a successful upload - same fields as the
|
|
3
3
|
# Go/JS/Python/PHP SDKs' UploadResult.
|
|
4
|
-
|
|
4
|
+
#
|
|
5
|
+
# path is the storage URL as uploaded and can change shortly afterwards
|
|
6
|
+
# when the file is converted (WebP/MP4) - store src, the permanent link.
|
|
7
|
+
# thumb is a 280 px WebP thumbnail (or nil); size is in bytes; duplicate is
|
|
8
|
+
# true when your account already had this exact file and that upload was
|
|
9
|
+
# returned.
|
|
10
|
+
# visibility is "private" (file only), "unlisted" (page for anyone with the
|
|
11
|
+
# link) or "public" (listed); delete_url (new uploads only) deletes the
|
|
12
|
+
# upload without an API key and is shown once.
|
|
13
|
+
UploadResult = Struct.new(:id, :type, :path, :src, :direct, :thumb, :width, :height, :size, :locked,
|
|
14
|
+
:visibility, :title, :description, :expires_at, :duplicate, :delete_url,
|
|
15
|
+
keyword_init: true) do
|
|
5
16
|
def self.from_json(h)
|
|
6
|
-
new(id: h["id"], type: h["type"], path: h["path"], src: h["src"],
|
|
7
|
-
|
|
17
|
+
new(id: h["id"], type: h["type"], path: h["path"], src: h["src"], direct: h["direct"],
|
|
18
|
+
thumb: h["thumb"], width: h["width"], height: h["height"], size: h["size"],
|
|
19
|
+
locked: h["locked"] == true, visibility: h["visibility"], title: h["title"],
|
|
20
|
+
description: h["description"], expires_at: h["expires_at"], duplicate: h["duplicate"] == true,
|
|
21
|
+
delete_url: h["delete_url"])
|
|
8
22
|
end
|
|
9
23
|
end
|
|
10
24
|
|
|
11
25
|
# One row of a #list result.
|
|
12
|
-
ListedUpload = Struct.new(:id, :type, :title, :path, :src, :direct, :
|
|
26
|
+
ListedUpload = Struct.new(:id, :type, :title, :path, :src, :direct, :thumb, :width, :height, :size, :locked,
|
|
27
|
+
:visibility, :description, :expires_at, :created_at, keyword_init: true) do
|
|
13
28
|
def self.from_json(h)
|
|
14
29
|
new(id: h["id"], type: h["type"], title: h["title"], path: h["path"],
|
|
15
|
-
src: h["src"], direct: h["direct"],
|
|
16
|
-
|
|
30
|
+
src: h["src"], direct: h["direct"], thumb: h["thumb"], width: h["width"], height: h["height"],
|
|
31
|
+
size: h["size"], locked: h["locked"] == true, visibility: h["visibility"],
|
|
32
|
+
description: h["description"], expires_at: h["expires_at"], created_at: h["created_at"])
|
|
17
33
|
end
|
|
18
34
|
end
|
|
19
35
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yourimageshare
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- YourImageShare
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08
|
|
11
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Upload, list, and delete files through the YourImageShare (https://yourimageshare.com)
|
|
14
14
|
JSON/REST API. Zero gem dependencies - standard library only.
|
|
@@ -30,8 +30,8 @@ licenses:
|
|
|
30
30
|
- MIT
|
|
31
31
|
metadata:
|
|
32
32
|
homepage_uri: https://yourimageshare.com/about/api
|
|
33
|
-
source_code_uri: https://github.com/
|
|
34
|
-
changelog_uri: https://github.com/
|
|
33
|
+
source_code_uri: https://github.com/yourimageshare/yourimageshare/tree/main/ruby
|
|
34
|
+
changelog_uri: https://github.com/yourimageshare/yourimageshare/tree/main/ruby/README.md
|
|
35
35
|
post_install_message:
|
|
36
36
|
rdoc_options: []
|
|
37
37
|
require_paths:
|