fileboy 0.2.0 → 0.3.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: f032a5828ce3db69a2ac9a2952774a2d5797906ee46c144af088f5bba1188a1e
4
- data.tar.gz: b625e71b7d308b922f4fd5ed9bd9821058d4a992732baab9cd64b2cfc8d5e245
3
+ metadata.gz: 8a84e114d8d513ec447db57039bd59c8535725bc0ebe278877574afcfa8eb8a1
4
+ data.tar.gz: 0f2c95d55b1ad22ffa35f620fc8daade39499d74919797c057d60ab7b30f713a
5
5
  SHA512:
6
- metadata.gz: 35732881e4adc1331d672f708e30618fc112e5f306093eec6cf27f0edc779a29617ee7124da5d2c7ff4f7abd431673757a85b585c0a1f4662de1e5f148af0ed9
7
- data.tar.gz: fb4739be49a85cd9645a20d6bed26e674b02e50c6e8e53c71594ec4bdbd102799e1c106d98e53d9d327ccf36efe91e2087038cb5c761f1583893b7b01cba2870
6
+ metadata.gz: 8be6951a4de0c4b97c777941873f933c10f774a67a1ae996b11ac3182917d723ec8867bdcac3e4757cbb1ac52c6b5550b395a302a6d67f5b2362fd8d582349a2
7
+ data.tar.gz: d9686d6a7d4758adbe1ec45ba94d6ee593e20cbfda476fbb7bf4d01bded65e95c476c2ce0c170c8253fa4b8173a1a80200c54f2f040d4a5f32000c46064cf680
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - New: `{ type: :format, format: :avif }` (and `:avif` as a `resize` format) asks for AVIF. `format: :auto` now serves AVIF to browsers that accept it, then WebP, with no change to the URL you build.
6
+ - New: overlay and text layers. `{ type: :overlay, file: logo_id, ... }` draws another image file of the same app on top (with `width`, `height` and its own `transforms`), and `{ type: :text, text: "...", ... }` writes text (`font`, `size`, `color`, `background`, `padding`, `wrap`, `align`). Both take `position`, `x`, `y`, `opacity`, `rotate`, and `tile` with `gap` for watermark patterns. Text is percent-encoded for you; `Fileboy::Transforms.encode_text` is the helper.
7
+ - New: a video's description, tags and metadata. `update_video` now takes `description:`, `tags:` (an Array replaces them, `{ add: [...], remove: [...] }` changes some) and `metadata:` (merged, nil removes a key) with `replace_metadata: true`, and `name:` is optional. `list_videos` filters by `tags:` (videos with every one) and `metadata:`, and `upload_video` takes all three.
8
+ - `Fileboy::ManagedVideo` and `Fileboy::Video` have `description`, `tags` and `metadata`.
9
+ - New: `fetch_file_exif(id_or_label)` returns a photo's EXIF, IPTC and XMP metadata as a `Fileboy::FileExif`: camera, lens, exposure, date taken (with its time zone when the file records one), orientation, GPS, colour profile and more as `Fileboy::ExifFields`, plus every raw tag. It needs the private key, because the GPS position and serial numbers are personal data; `personal: false` leaves those out.
10
+ - New: `fetch_file_palette(id_or_label, colors: 6)` returns an image's colour palette as a Hash: the dominant colour, up to 16 main colours with their share of the image, the named swatches (vibrant, muted and their dark and light versions) and a readable text colour for each. Needs the private key.
11
+ - New: `{ type: :palette }` builds the palette transform, which answers with the colours of the image as transformed so far: `output: :css` for CSS custom properties and classes, `colors:` (0 to 16) and, for CSS, `prefix:`.
12
+ - New: `fetch_storage_usage(days: 30, top: 10)` returns what your app stores by kind (images, other files, video originals, renditions, posters, captions and cached image copies), your biggest files and videos, and one total a day. `storage_bytes` is the same total `fetch_usage` reports. Needs the private key.
13
+ - New: folders. `create_folder(name, parent_id:)` or `create_folder(path:)` (creates every missing folder on the path), `list_folders` and `each_folder` (by parent, name or exact path), `fetch_folder` (with `ancestors`), `update_folder` (rename, or move with `parent_id:`, `nil` for the root) and `delete_folder` (`contents: :move_to_parent` or `:delete` for a folder that is not empty). They return a `Fileboy::Folder`.
14
+ - New: `move_files(ids, folder_id:)` moves up to 100 files at once, `nil` for the root, or `folder_path:`. `update_file` takes `folder_id:` and `folder_path:` too.
15
+ - `list_files` filters by `folder_id:` (or `"root"`), `folder_path:` and `recursive:`. `upload_file` and `upload_file_from_url` take `folder_id:` or `folder_path:`.
16
+ - `Fileboy::FileData` has `folder_id` and `folder_path`.
17
+ - New: expiring links you can revoke. `create_file_link(id, expires_in: 7 * 24 * 3600)` makes a URL to one file (optionally an image transform, as a download, under a filename) that works until it expires or you call `revoke_file_link(id, link_id)`, even when the file or app needs signed reads. `list_file_links` and `each_file_link` list a file's links with their use counts. Needs the private key; results are `Fileboy::FileLink` structs.
18
+ - `link_not_found` raises `NotFoundError`; `too_many_links` (100 active links on a file) and `invalid_transform` raise `BadRequestError`.
19
+ - New: share pages. `update_file(id, share_page: true)` turns a file's share page on, `build_share_page_url(id)` gives its link and `sign_share_page_url(id, expires_in: ...)` a signed link for a file that needs a signed URL. `FileData#share_page` says whether it is on, and `Client.new(website_url: ...)` builds links on another host. `sign_url` also signs share page URLs (`/f/{id}`, scope `share`).
20
+
21
+ - New: `build_video_thumbnails_url(video_id)` is the URL of a video's seek-preview thumbnails, a WebVTT file with a sprite sheet that the Fileboy player, Plyr, Video.js and most other players read. `format: :json` gives the same as a JSON storyboard.
22
+ - New: signed URLs for apps that require signed reads. `client.sign_url(url, expires_in: 600)` signs the URL of a file, image or video (including every transform URL) with your private key, `client.sign_video(video_id)` returns the query string that unlocks every URL of one video, and `Fileboy::Signing` has the lower level pieces. Signing needs the private key and happens on your server.
23
+ - New: `fetch_require_signed_reads` and `update_require_signed_reads(true)` read and change the app's setting from code.
24
+ - Signatures are identical across the Node, Ruby, Python and PHP SDKs, they test against the same fixtures (`packages/sdk-fixtures/signing.json`).
25
+ - New: every image transform the service supports can be built. Added `resizeAR`, `crop`, `smartCrop`, `rotate`, `flip`, `blur`, `sharpen`, `saturate`, `grayscale`, `brightness`, `contrast`, `hue`, `gamma`, `sepia`, `tint`, `opacity`, `pixelate`, `radius`, `pad`, `border`, `dpr` and `strip`, `format: :auto` and a `focus` point for `resize`.
26
+ - Colours can now be `rgb(...)`, `rgba(...)` or a CSS colour name as well as hex.
27
+ - New: `upscale: false` on `resize` never makes an image bigger than it is (`~upscale:false`), `trim` cuts off a plain border (optional `threshold` and `color`), `lossless` asks for lossless WebP, and `{ type: :format, format: :json }` returns the image's details as JSON. `json` is not accepted as a `resize` format. In the hash form `{ trim: true }`, `{ trim: 20 }` and `{ lossless: true }` work too.
28
+ - The hash form skips `false` values, so `{ grayscale: flag }` only adds the transform when `flag` is true (it used to send `grayscale:false`).
29
+ - The `transform` value in image URLs is percent-encoded where needed (`+`, `&`, `#`, `%`, spaces), so a number like `1e+21` reaches the image service intact. Ordinary transforms give the same URL as before.
30
+
3
31
  ## 0.2.0
4
32
 
5
33
  - New: manage your app from Ruby. List, change and delete files, videos and labels, and read your usage. These calls need the private key and raise `Fileboy::ConfigurationError` before any request if the client has none.
data/README.md CHANGED
@@ -44,15 +44,16 @@ end
44
44
 
45
45
  | Task | Method |
46
46
  | --- | --- |
47
- | Upload a path or IO | `upload_file(file, name:, mime:, metadata:)` |
48
- | Upload from a URL | `upload_file_from_url(url, name:, metadata:, expect:)` |
47
+ | Upload a path or IO | `upload_file(file, name:, mime:, metadata:, folder_id:, folder_path:)` |
48
+ | Upload from a URL | `upload_file_from_url(url, name:, metadata:, expect:, folder_id:, folder_path:)` |
49
49
  | File details | `fetch_file_data(id_or_label)` |
50
50
  | File URL | `build_file_url(id_or_label, filename:, download:)` |
51
51
  | Image URL with transforms | `build_image_url(id_or_label, transforms:, filename:, download:)` |
52
52
  | Download bytes | `download_file`, `download_image` |
53
- | Upload a video | `upload_video(file, name:)` |
53
+ | Upload a video | `upload_video(file, name:, description:, tags:, metadata:)` |
54
54
  | Video details | `fetch_video_data(id)`, `fetch_video_info(id)` |
55
55
  | Manifest and poster URLs | `build_video_manifest_url(id, type: :hls)`, `build_video_poster_url(id)` |
56
+ | Seek-preview thumbnails | `build_video_thumbnails_url(id)` (WebVTT), `build_video_thumbnails_url(id, format: :json)` |
56
57
  | Custom poster | `upload_video_poster(video_id, image)` |
57
58
  | Transcripts | `upload_video_transcript(id, file, language: "English")`, `fetch_video_transcript(id, transcript_id)` |
58
59
  | Signed access token | `create_access_token(read:, write:, upload:, expires_in:)` |
@@ -62,21 +63,70 @@ created in the dashboard.
62
63
 
63
64
  ### Image transforms
64
65
 
65
- `transforms:` takes an array of hashes applied in order. The image service supports `resize`,
66
- `format`, `quality`, `background` and `invert`:
66
+ `transforms:` takes an array of hashes applied in order. Pixel sizes are whole numbers; where a
67
+ percentage is allowed write `{ pc: 50 }` (or `"50pc"`). Colours can be `#rgb`, `#rrggbb`,
68
+ `#rrggbbaa`, `rgb(...)`, `rgba(...)` or a CSS name like `"white"`.
67
69
 
68
70
  ```ruby
69
71
  [
72
+ # Size and shape
70
73
  { type: :resize, width: 200, height: 200, fit: :cover }, # fit: contain cover fill inside outside
71
74
  { type: :resize, width: 400, fit: :contain, position: "top-left", background: "#ffffff" },
72
- { type: :format, format: :webp }, # jpeg png webp tiff bmp
75
+ { type: :resize, width: 300, height: 300, focus: { x: { pc: 30 }, y: { pc: 70 } } }, # keep this point in view
76
+ { type: :resize, width: 800, upscale: false }, # never bigger than the original
77
+ { type: :resizeAR, ratio: [16, 9], fit: :contain, background: "black" },
78
+ { type: :crop, width: 400, height: 300, left: { pc: 50 }, top: 120 }, # centre of the area
79
+ { type: :crop, width: { pc: 50 }, height: { pc: 50 }, position: "top-left" },
80
+ { type: :smartCrop, width: 400, height: 400 }, # crops to the most detailed area
81
+ { type: :trim, threshold: 10, color: "white" }, # cuts off a plain border; both optional
82
+ { type: :rotate, angle: 90 }, # clockwise; other angles leave a background
83
+ { type: :flip, direction: :x }, # x y xy
84
+ { type: :radius, radius: 24 }, # or "max" for a circle
85
+ { type: :pad, padding: [10, 20], background: "#fff" }, # all, [v, h] or [top, right, bottom, left]
86
+ { type: :border, width: 4, color: "#000" },
87
+ { type: :dpr, ratio: 2 }, # multiplies every pixel size
88
+ # Look
89
+ { type: :blur, amount: 5 }, # 0 to 100
90
+ { type: :sharpen }, # or amount: 0 to 10
91
+ { type: :grayscale },
92
+ { type: :saturate, amount: 1.5 }, # 0 grey, 1 unchanged
93
+ { type: :brightness, amount: 10 }, # -100 to 100
94
+ { type: :contrast, amount: 10 }, # -100 to 100
95
+ { type: :hue, degrees: 90 },
96
+ { type: :gamma, value: 1.2 },
97
+ { type: :sepia }, # or amount: 0 to 100
98
+ { type: :tint, color: "#ff0000" },
99
+ { type: :opacity, amount: 50 },
100
+ { type: :pixelate, size: 12 },
101
+ { type: :invert },
102
+ # Layers: another image file of the same app, or text, drawn on top
103
+ { type: :overlay, file: logo_id, position: "bottom-right", x: 20, y: 20, width: { pc: 20 }, opacity: 80 },
104
+ { type: :overlay, file: logo_id, tile: true, gap: 40, rotate: -30, opacity: 25 }, # a watermark pattern
105
+ { type: :overlay, file: logo_id, transforms: [{ type: :grayscale }, { type: :radius, radius: "max" }] },
106
+ { type: :text, text: "© 2026 Fileboy", font: "sans-bold", size: { pc: 4 }, color: "#fff",
107
+ background: "#00000080", padding: 8, position: "bottom-left", x: 16, y: 16 },
108
+ # Output
109
+ { type: :format, format: :auto }, # auto (AVIF, then WebP, when the browser takes them) avif webp jpeg png tiff bmp
110
+ { type: :format, format: :json }, # the image's details as JSON, not an image
111
+ { type: :palette, output: :css, colors: 4 }, # the main colours as JSON (default) or CSS, not an image
73
112
  { type: :quality, value: 75 },
74
- { type: :background, color: "#fff" },
75
- { type: :invert }
113
+ { type: :lossless }, # lossless WebP or AVIF
114
+ { type: :background, color: "#fff" }, # default for transforms that leave space
115
+ { type: :strip } # re-encode without metadata
76
116
  ]
77
117
  ```
78
118
 
79
- You can also pass a hash of `name => value`, such as `{ resize: "300x_", format: "webp", quality: 75 }`, or a ready-made string.
119
+ Overlays and text take `position` (default `center`), `x` and `y` (moved in from that edge, pixels
120
+ or `{ pc: n }`, negative allowed), `opacity` (0 to 100), `rotate`, and `tile` with `gap`. An overlay
121
+ is the id of an image file in the same app; `width` and `height` size it (one keeps its shape, both
122
+ fit it inside the box) and `transforms` run on it first. Text is encoded for you
123
+ (`Fileboy::Transforms.encode_text`), in the fonts `sans`, `sans-bold`, `sans-italic`, `mono` or
124
+ `mono-bold`, with `size` (pixels, or `{ pc: n }` of the image's height), `color`, `background`,
125
+ `padding`, `wrap` and `align`. If the overlay file is set to signed URLs only, sign the image URL:
126
+ the signature covers the overlay.
127
+
128
+ You can also pass a hash of `name => value`, such as `{ resize: "300x_", format: "webp", grayscale: true }`, or a ready-made string.
129
+ The builder checks what it can before you make a request; the image service answers anything it cannot apply with a 400 that says why.
80
130
 
81
131
  ## Managing files
82
132
 
@@ -103,16 +153,38 @@ fileboy.update_file("hero", name: "x") # a label name works too
103
153
  fileboy.delete_file(file.id) # raises Fileboy::NotFoundError if it is gone
104
154
  result = fileboy.delete_files([id1, id2]) # up to 100; result.deleted, .not_found, .failed
105
155
 
106
- fileboy.list_videos(status: :ready).items
156
+ fileboy.list_videos(status: :ready, tags: ["launch"], metadata: { orderId: "42" }).items
107
157
  fileboy.update_video(video_id, name: "Launch.mp4")
158
+ # The description shows on the watch page. Tags and metadata are private, for your own records.
159
+ video = fileboy.update_video(video_id, description: "Launch day", tags: { add: ["launch"], remove: ["draft"] },
160
+ metadata: { orderId: "42", old: nil })
161
+ video.description, video.tags, video.metadata # also in every video.* webhook
108
162
  fileboy.delete_video(video_id)
109
163
 
110
164
  fileboy.list_labels # [Fileboy::Label]
111
165
  fileboy.create_label("hero", file_id: file.id) # BadRequestError with code "label_taken" if it exists
112
166
  fileboy.delete_label("hero") # the file is kept
113
167
 
168
+ # An image's colours: the dominant one, up to 16 main colours with their share of the image,
169
+ # vibrant and muted swatches, and a readable text colour (black or white) for each
170
+ palette = fileboy.fetch_file_palette(file.id, colors: 4)
171
+ palette["dominant"]["hex"]; palette["colors"].first["share"]; palette["swatches"]["vibrant"]
172
+
114
173
  usage = fileboy.fetch_usage
115
- usage.files; usage.storage_bytes; usage.limits.storage_bytes # a nil limit means none is set
174
+ usage.files; usage.storage_bytes; usage.limits.storage_bytes # nil: no limit set on the app (not unlimited)
175
+
176
+ # A photo's camera, lens, exposure, date taken and GPS position (EXIF, IPTC and XMP)
177
+ exif = fileboy.fetch_file_exif(file.id)
178
+ exif.fields.model; exif.fields.aperture; exif.fields.shutter_speed; exif.fields.date_taken
179
+ exif.fields.gps # Fileboy::ExifGps (latitude, longitude, altitude) or nil
180
+ exif.raw["exif"]["LensModel"] # every tag read, by source
181
+ fileboy.fetch_file_exif(file.id, personal: false) # without GPS, place names, serial numbers or owner
182
+
183
+ # What the storage is made of, the biggest files and videos, and a daily total
184
+ storage = fileboy.fetch_storage_usage(days: 90, top: 5)
185
+ storage.kinds["videoRenditions"].bytes # also images, files, videoOriginals, imageTransforms...
186
+ storage.largest_files.first.id; storage.largest_videos.first.storage_bytes
187
+ storage.history.map { |day| [day.date, day.storage_bytes] }
116
188
  ```
117
189
 
118
190
  Failures carry the API's `code`, message and HTTP `status`:
@@ -126,6 +198,102 @@ rescue Fileboy::NotFoundError => e
126
198
  end
127
199
  ```
128
200
 
201
+ ### Expiring links
202
+
203
+ An expiring link is a URL to one file that works until it expires or you revoke it. Hand it to a
204
+ customer, an auditor or a partner, and take it back if you need to. Needs the private key.
205
+
206
+ ```ruby
207
+ # A link that works for 7 days
208
+ link = fileboy.create_file_link(file.id, expires_in: 7 * 24 * 3600, note: "for the auditors")
209
+ link.url # https://api.fileboy.io/links/...
210
+ link.expires_at # a Time
211
+
212
+ # A resized WebP of an image, downloaded as logo.webp, until a set time
213
+ fileboy.create_file_link("logo", expires_at: Time.utc(2026, 12, 1, 9), transform: "resize:400x_;format:webp",
214
+ download: true, filename: "logo.webp")
215
+
216
+ page = fileboy.list_file_links(file.id, status: :active) # newest first; status: :all is the default
217
+ page.each { |l| puts "#{l.id} #{l.status} #{l.use_count} #{l.last_used_at}" }
218
+ fileboy.each_file_link(file.id).to_a # every page
219
+
220
+ fileboy.revoke_file_link(file.id, link.id) # stops working at once, safe to repeat
221
+ ```
222
+
223
+ Give exactly one of `expires_in` (seconds) or `expires_at` (a `Time`, `DateTime` or ISO 8601
224
+ string), from one minute to 365 days away. A file can have at most 100 active links
225
+ (`BadRequestError` with code `too_many_links`). Results are `Fileboy::FileLink` structs (`id`,
226
+ `file_id`, `url`, `status`, `expires_at`, `revoked_at`, `transform`, `download`, `filename`, `note`,
227
+ `created_via`, `created_at`, `last_used_at`, `use_count`).
228
+
229
+ A link works until it expires or is revoked even if the file is "Signed URLs only" or the app
230
+ requires signed reads. It does not make a public file private: the file's own URL keeps working.
231
+ For a link per page view, `sign_url` is the better tool (no request, nothing stored); stored links
232
+ are for links you hand out and might take back.
233
+
234
+ ## Folders
235
+
236
+ Folders organise files. They are never part of a URL, so moving or renaming one changes no links.
237
+ Every file starts at the root. These calls need the private key, like the rest of the management calls.
238
+
239
+ ```ruby
240
+ shoes = fileboy.create_folder(path: "/Products/Shoes") # creates what is missing, like mkdir -p
241
+ sale = fileboy.create_folder("Sale", parent_id: shoes.id) # BadRequestError "folder_exists" if it is taken
242
+
243
+ file = fileboy.upload_file("shoe.png", folder_path: "/Products/Shoes") # or folder_id: shoes.id
244
+ file.folder_id; file.folder_path
245
+
246
+ fileboy.list_files(folder_id: shoes.id) # only the files directly in it
247
+ fileboy.list_files(folder_path: "/Products", recursive: true) # subfolders included
248
+ fileboy.list_files(folder_id: "root") # files in no folder
249
+
250
+ fileboy.update_file(file.id, folder_path: "/Archive") # one file; folder_id: nil moves it to the root
251
+ result = fileboy.move_files([id1, id2], folder_id: shoes.id) # up to 100; result.moved, .not_found, .folder
252
+ fileboy.move_files([id1], folder_id: nil) # to the root
253
+
254
+ fileboy.list_folders(parent_id: "root").items # top level, by name
255
+ fileboy.list_folders(path: "/Products/Shoes").items # [] or the one folder
256
+ fileboy.fetch_folder(shoes.id).ancestors # the folders above, for breadcrumbs
257
+
258
+ fileboy.update_folder(shoes.id, name: "Boots")
259
+ fileboy.update_folder(shoes.id, parent_id: nil) # move it to the root
260
+
261
+ fileboy.delete_folder(sale.id) # only an empty folder, else "folder_not_empty"
262
+ fileboy.delete_folder(shoes.id, contents: :move_to_parent) # its files and folders move up a level
263
+ fileboy.delete_folder(shoes.id, contents: :delete) # deletes the files too, at most 1000
264
+ ```
265
+
266
+ Uploads made with only the public key, or an access token, can use folders that exist but cannot
267
+ create them.
268
+
269
+ ## Signed URLs
270
+
271
+ If your app requires signed reads, sign URLs on your server with the private key:
272
+
273
+ ```ruby
274
+ url = Fileboy.client.sign_url(Fileboy.client.build_image_url(id, transforms: { resize: [400, 400] }), expires_in: 600)
275
+ query = Fileboy.client.sign_video(video_id, expires_in: 4 * 3600) # add to any /videos/{id}/... URL
276
+ ```
277
+
278
+ Signing makes no request. See https://fileboy.io/docs/delivery/signed-urls.
279
+
280
+ ## Share pages
281
+
282
+ A share page is a page anyone can open at `https://fileboy.io/f/{id}`, with a preview of the file
283
+ and a download button. It is off until you turn it on, and it follows the file's read access:
284
+
285
+ ```ruby
286
+ Fileboy.client.update_file(id, share_page: true)
287
+ link = Fileboy.client.build_share_page_url(id) # https://fileboy.io/f/2a86...
288
+
289
+ # A file set to Signed URLs only, or any file of an app that requires signed reads, only opens
290
+ # through a signed share link. Its preview and download stop working when the link does.
291
+ signed_link = Fileboy.client.sign_share_page_url(id, expires_in: 7 * 24 * 3600)
292
+ ```
293
+
294
+ `update_file(id, share_page: false)` turns the page off, and every link to it stops working.
295
+ `FileData#share_page` says whether it is on. See https://fileboy.io/docs/delivery/share-pages.
296
+
129
297
  ## Errors
130
298
 
131
299
  Every error is a `Fileboy::Error` with `status`, `code` (the API's error code) and `body`.