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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee8bed4a3f8b3383ae8ca06a4031231c404aa4c843bc65578ff7b2d8b8c1eed4
4
- data.tar.gz: 5f7201b52c57bb2d283da2730ef68d79386d2748bb03c99b8fc9348f5e3e1e8c
3
+ metadata.gz: 1b5d4188f0b04cf69a8275bc809330e30bd0e5d9b7451a81b5080c5db3b26696
4
+ data.tar.gz: 3c03786f86644591965adce714cb982b6c3ba5b354041c4b1275704e88d8ffa0
5
5
  SHA512:
6
- metadata.gz: 2fa5756c5aa8d1f779301b731b6532e70f0d99ea068d4f0f6baa7b2361fef9c5857a817909c76c54054f6bf554d6b97fa59fbc29f70828ea3011490e7fb9a5e1
7
- data.tar.gz: 10571f3b239e22ee6dd9b544a1db000938dde0a7f03110cb64f52c6e0826a586c8e01b4376a7d1934594c3a9c8c5ea9a6b1731b642f9d33b0b91ac5dde283926
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/MediaShareORG/yourimageshare/go) too.
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 2,592,000 =
21
- # 30 days) auto-deletes the upload later; nil means a permanent upload.
22
- def upload(file_path, expires_in: nil)
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
- uri = URI(@base_url)
32
- request = Net::HTTP::Post.new(uri)
33
- set_common_headers(request)
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 - important since uploads can be up to
39
- # 200MB (video).
40
- request.set_form(form, "multipart/form-data")
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
- body = execute(uri, request)
43
- UploadResult.from_json(body["data"] || {})
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: @timeout) do |http|
163
+ open_timeout: @timeout, read_timeout: read_timeout, write_timeout: read_timeout) do |http|
78
164
  http.request(request)
79
165
  end
80
166
 
@@ -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
- UploadResult = Struct.new(:id, :type, :path, :src, :direct, :expires_at, keyword_init: true) do
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
- direct: h["direct"], expires_at: h["expires_at"])
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, :expires_at, :created_at, keyword_init: true) do
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"], expires_at: h["expires_at"],
16
- created_at: h["created_at"])
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
 
@@ -1,3 +1,3 @@
1
1
  module YourImageShare
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
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.0.0
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-27 00:00:00.000000000 Z
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/MediaShareORG/yourimageshare/tree/main/ruby
34
- changelog_uri: https://github.com/MediaShareORG/yourimageshare/tree/main/ruby/README.md
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: