Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: image transformation #128

Merged
merged 1 commit into from Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.