Skip to content

Commit

Permalink
feat: image transformation (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos committed Nov 29, 2022
1 parent 20ab562 commit 16fbe5b
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 19 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Expand Up @@ -7,6 +7,7 @@ on:
- main
- next
- rc
- next-rc-1
workflow_dispatch:

jobs:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Expand Up @@ -6,6 +6,7 @@ on:
- main
- next
- rc
- next-rc-1
workflow_dispatch:

jobs:
Expand Down
33 changes: 29 additions & 4 deletions infra/docker-compose.yml
Expand Up @@ -17,20 +17,22 @@ services:
ports:
- '3000:3000'
depends_on:
- db
storage:
condition: service_healthy
restart: always
environment:
PGRST_DB_URI: postgres://postgres:postgres@db:5432/postgres
PGRST_DB_SCHEMA: public, storage
PGRST_DB_ANON_ROLE: postgres
PGRST_JWT_SECRET: super-secret-jwt-token-with-at-least-32-characters-long
storage:
image: supabase/storage-api:v0.20.2
build:
context: ./storage
ports:
- '5000:5000'
depends_on:
- db
- rest
db:
condition: service_healthy
restart: always
environment:
ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMzUzMTk4NSwiZXhwIjoxOTI5MTA3OTg1fQ.ReNhHIoXIOa-8tL1DO3e26mJmOTnYuvdgobwIYGzrLQ
Expand All @@ -47,6 +49,12 @@ services:
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /tmp/storage
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:8080
volumes:
- assets-volume:/tmp/storage
healthcheck:
test: ['CMD-SHELL', 'curl -f -LI http://localhost:5000/status']
db:
build:
context: ./postgres
Expand All @@ -61,3 +69,20 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_PORT: 5432
healthcheck:
test: [ "CMD-SHELL", "pg_isready" ]
interval: 10s
timeout: 5s
retries: 5

imgproxy:
image: darthsim/imgproxy
ports:
- 50020:8080
volumes:
- assets-volume:/tmp/storage
environment:
- IMGPROXY_LOCAL_FILESYSTEM_ROOT=/
- IMGPROXY_USE_ETAG=true
volumes:
assets-volume:
4 changes: 3 additions & 1 deletion infra/postgres/dummy-data.sql
Expand Up @@ -51,4 +51,6 @@ CREATE POLICY authenticated_folder ON storage.objects for all USING (bucket_id='
-- allow CRUD access to a folder in bucket2 to its owners
CREATE POLICY crud_owner_only ON storage.objects for all USING (bucket_id='bucket2' and (storage.foldername(name))[1] = 'only_owner' and owner = auth.uid());
-- allow CRUD access to bucket4
CREATE POLICY open_all_update ON storage.objects for all WITH CHECK (bucket_id='bucket4');
CREATE POLICY open_all_update ON storage.objects for all WITH CHECK (bucket_id='bucket4');

CREATE POLICY crud_my_bucket ON storage.objects for all USING (bucket_id='my-private-bucket' and auth.uid()::text = '317eadce-631a-4429-a0bb-f19a7a517b4a');
3 changes: 3 additions & 0 deletions infra/storage/Dockerfile
@@ -0,0 +1,3 @@
FROM supabase/storage-api:v0.24.10

RUN apk add curl --no-cache
6 changes: 6 additions & 0 deletions src/lib/types.ts
Expand Up @@ -72,3 +72,9 @@ export interface FetchParameters {
export interface Metadata {
name: string
}

export interface TransformOptions {
width?: number
height?: number
resize?: 'fill' | 'fit' | 'fill-down' | 'force' | 'auto'
}
122 changes: 110 additions & 12 deletions src/packages/StorageFileApi.ts
@@ -1,7 +1,13 @@
import { isStorageError, StorageError } from '../lib/errors'
import { Fetch, get, post, remove } from '../lib/fetch'
import { resolveFetch } from '../lib/helpers'
import { FileObject, FileOptions, SearchOptions, FetchParameters } from '../lib/types'
import {
FileObject,
FileOptions,
SearchOptions,
FetchParameters,
TransformOptions,
} from '../lib/types'

const DEFAULT_SEARCH_OPTIONS = {
limit: 100,
Expand Down Expand Up @@ -263,7 +269,7 @@ export default class StorageFileApi {
async createSignedUrl(
path: string,
expiresIn: number,
options?: { download: string | boolean }
options?: { download?: string | boolean; transform?: TransformOptions }
): Promise<
| {
data: { signedUrl: string }
Expand All @@ -275,11 +281,12 @@ export default class StorageFileApi {
}
> {
try {
const _path = this._getFinalPath(path)
let _path = this._getFinalPath(path)

let data = await post(
this.fetch,
`${this.url}/object/sign/${_path}`,
{ expiresIn },
{ expiresIn, ...(options?.transform ? { transform: options.transform } : {}) },
{ headers: this.headers }
)
const downloadQueryParam = options?.download
Expand Down Expand Up @@ -347,13 +354,60 @@ export default class StorageFileApi {
}
}

/**
* Download a file in a public bucket
* @param path
* @param options
*/
publicDownload(
path: string,
options?: {
transform?: TransformOptions
}
) {
const wantsTransformations = typeof options?.transform !== 'undefined'
const renderPath = wantsTransformations ? 'render/image' : 'object'
const queryString = this.transformOptsToQueryString(options?.transform || {})

return this.download(path, {
prefix: `${renderPath}/public`,
queryString: queryString,
})
}

/**
* Download a file in a private bucket
* @param path
* @param options
*/
authenticatedDownload(
path: string,
options?: {
transform?: TransformOptions
}
) {
const wantsTransformations = typeof options?.transform !== 'undefined'
const renderPath = wantsTransformations ? 'render/image' : 'object'
const queryString = this.transformOptsToQueryString(options?.transform || {})

return this.download(path, {
prefix: `${renderPath}/authenticated`,
queryString: queryString,
})
}

/**
* Downloads a file.
*
* @param path The full path and file name of the file to be downloaded. For example `folder/image.png`.
* @deprecated use publicDownload or authenticatedDownload
*/
async download(
path: string
path: string,
options?: {
prefix?: string
queryString?: string
}
): Promise<
| {
data: Blob
Expand All @@ -366,10 +420,17 @@ export default class StorageFileApi {
> {
try {
const _path = this._getFinalPath(path)
const res = await get(this.fetch, `${this.url}/object/${_path}`, {
headers: this.headers,
noResolveJson: true,
})
const renderPath = options?.prefix ?? 'object'
const queryString = options?.queryString

const res = await get(
this.fetch,
`${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ''}`,
{
headers: this.headers,
noResolveJson: true,
}
)
const data = await res.blob()
return { data, error: null }
} catch (error) {
Expand All @@ -387,18 +448,38 @@ export default class StorageFileApi {
*
* @param path The path and name of the file to generate the public URL for. For example `folder/image.png`.
* @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
* @param options.transform adds image transformations parameters to the generated url
*/
getPublicUrl(
path: string,
options?: { download: string | boolean }
options?: { download?: string | boolean; transform?: TransformOptions }
): { data: { publicUrl: string } } {
const _path = this._getFinalPath(path)
const _queryString = []

const downloadQueryParam = options?.download
? `?download=${options.download === true ? '' : options.download}`
? `download=${options.download === true ? '' : options.download}`
: ''

if (downloadQueryParam !== '') {
_queryString.push(downloadQueryParam)
}

const wantsTransformation = typeof options?.transform !== 'undefined'
const renderPath = wantsTransformation ? 'render/image' : 'object'
const transformationQuery = this.transformOptsToQueryString(options?.transform || {})

if (transformationQuery !== '') {
_queryString.push(transformationQuery)
}

let queryString = _queryString.join('&')
if (queryString !== '') {
queryString = `?${queryString}`
}

return {
data: { publicUrl: encodeURI(`${this.url}/object/public/${_path}${downloadQueryParam}`) },
data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) },
}
}

Expand Down Expand Up @@ -543,4 +624,21 @@ export default class StorageFileApi {
private _removeEmptyFolders(path: string) {
return path.replace(/^\/|\/$/g, '').replace(/\/+/g, '/')
}

private transformOptsToQueryString(transform: TransformOptions) {
const params = []
if (transform.width) {
params.push(`width=${transform.width}`)
}

if (transform.height) {
params.push(`height=${transform.height}`)
}

if (transform.resize) {
params.push(`resize=${transform.resize}`)
}

return params.join('&')
}
}
Binary file added test/fixtures/upload/sadcat.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 16fbe5b

Please sign in to comment.