aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.42k stars 2.12k forks source link

Feature Request S3Image support customPrefix #2359

Closed 10ky closed 5 years ago

10ky commented 5 years ago

Which Category is your question related to? AWS Amplify S3Image

What AWS Services are you utilizing? S3

Provide additional details e.g. code snippets I can put a photo to S3 using customPrefix:

const customPrefix = {
  public: 'public1',
  protected: 'protected1',
  private: 'private1'
};
    await Storage.vault.put(
      fileName,
      file,
      {
        customPrefix: customPrefix
      }
    )

I want to show the photo using S3Image like so:

      <S3Image
        level="private"
        customPrefix=??
        key={photo.thumbnail.key} 
        imgKey={photo.thumbnail.key} 
        style={{display: 'inline-block', 'paddingRight': '5px'}}
      />

S3Image currently does not support customPrefix:

    getImageSource(key, level, track) {
        if (!Storage || typeof Storage.get !== 'function') {
            throw new Error('No Storage module found, please ensure @aws-amplify/storage is imported');
        }
        Storage.get(key, { level: level? level : 'public', track })
            .then(url => {
                this.setState({
                    src: url
                });
            })
            .catch(err => logger.debug(err));
    }

https://github.com/aws-amplify/amplify-js/blob/88ccc4f680d616751d679299dc9740af115332e8/packages/aws-amplify-react/src/Storage/S3Image.js

If it is not a security risk to add customPrefix, please consider adding this feature.

10ky commented 5 years ago

The changes required should not be much, something like this:

/*
 * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
 * the License. A copy of the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
 * and limitations under the License.
 */

import * as React from 'react';
import { Component } from 'react';

import { ConsoleLogger as Logger } from '@aws-amplify/core';
import Storage from '@aws-amplify/storage';

import AmplifyTheme from '../AmplifyTheme';
import { transparent1X1 } from '../AmplifyUI';
import PhotoPicker from '../Widget/PhotoPicker';
import { calcKey } from './Common';

const logger = new Logger('Storage.S3Image');

export default class S3Image extends Component {
    constructor(props) {
        super(props);

        this.handleOnLoad = this.handleOnLoad.bind(this);
        this.handleOnError = this.handleOnError.bind(this);
        this.handlePick = this.handlePick.bind(this);
        this.handleClick = this.handleClick.bind(this);

        const initSrc = this.props.src || transparent1X1;

        this.state = { src: initSrc };
    }

    getImageSource(key, level, customPrefix, track) {
        if (!Storage || typeof Storage.get !== 'function') {
            throw new Error('No Storage module found, please ensure @aws-amplify/storage is imported');
        }
        Storage.get(key, {
            customPrefix: customPrefix ? customPrefix : { public: 'public', protected: 'protected', private: 'private' },
            level: level ? level : 'public', 
            track
        })
            .then(url => {
                this.setState({
                    src: url
                });
            })
            .catch(err => logger.debug(err));
    }

    load() {
        const { imgKey, path, body, contentType, level, customPrefix, track } = this.props;
        if (!imgKey && !path) {
            logger.debug('empty imgKey and path');
            return;
        }

        const that = this;
        const key = imgKey || path;
        logger.debug('loading ' + key + '...');
        if (body) {
            const type = contentType || 'binary/octet-stream';
            if (!Storage || typeof Storage.put !== 'function') {
                throw new Error('No Storage module found, please ensure @aws-amplify/storage is imported');
            }
            const ret = Storage.put(key, body, {
                contentType: type,
                level: level ? level : 'public',
                track
            });
            ret.then(data => {
                logger.debug(data);
                that.getImageSource(key, level, customPrefix, track);
            })
                .catch(err => logger.debug(err));
        } else {
            that.getImageSource(key, level, customPrefix, track);
        }
    }

    handleOnLoad(evt) {
        const { onLoad } = this.props;
        if (onLoad) { onLoad(this.state.src); }
    }

    handleOnError(evt) {
        const { onError } = this.props;
        if (onError) { onError(this.state.src); }
    }

    handlePick(data) {
        const that = this;

        const path = this.props.path || '';
        const { imgKey, level, fileToKey, track } = this.props;
        const { file, name, size, type } = data;
        const key = imgKey || (path + calcKey(data, fileToKey));
        if (!Storage || typeof Storage.put !== 'function') {
            throw new Error('No Storage module found, please ensure @aws-amplify/storage is imported');
        }
        Storage.put(key, file, {
            level: level ? level : 'public',
            contentType: type,
            track
        })
            .then(data => {
                logger.debug('handle pick data', data);
                that.getImageSource(key, level, getImageSource, track);
            })
            .catch(err => logger.debug('handle pick error', err));
    }

    handleClick(evt) {
        const { onClick } = this.props;
        if (onClick) { onClick(evt); }
    }

    componentDidMount() {
        this.load();
    }

    componentDidUpdate(prevProps) {
        const update = prevProps.path !== this.props.path ||
            prevProps.imgKey !== this.props.imgKey ||
            prevProps.body !== this.props.body;
        if (update) {
            this.load();
        }
    }

    imageEl(src, theme) {
        if (!src) { return null; }

        const { selected } = this.props;
        const containerStyle = { position: 'relative' };
        return (
            <div style={containerStyle} onClick={this.handleClick}>
                <img
                    style={theme.photoImg}
                    src={src}
                    onLoad={this.handleOnLoad}
                    onError={this.handleOnError}
                />
                <div style={selected ? theme.overlaySelected : theme.overlay}></div>
            </div>
        );
    }

    render() {
        const { hidden, style, picker, translate, imgKey } = this.props;
        let src = this.state.src;
        if (translate) {
            src = (typeof translate === 'string') ? translate : translate({
                type: 'image',
                key: imgKey,
                content: src
            });
        }
        if (!src && !picker) { return null; }

        const theme = this.props.theme || AmplifyTheme;
        const photoStyle = hidden ? AmplifyTheme.hidden
            : Object.assign({}, theme.photo, style);

        return (
            <div style={photoStyle}>
                {photoStyle ? this.imageEl(src, theme) : null}
                {picker ? <div>
                    <PhotoPicker
                        key="picker"
                        onPick={this.handlePick}
                        theme={theme}
                    />
                </div> : null
                }
            </div>
        );
    }
}
haverchuck commented 5 years ago

@10ky Have you tried using Storage.configure() to set the customPrefix?

10ky commented 5 years ago

Just tried this:

    Storage.configure({
      customPrefix: { public: 'public/', protected: 'protected/', private: 'private_resized/' }
    })

Also updated my IAM role for auth user:

    "Statement": [
        {
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::photos/public/*",
                "arn:aws:s3:::photos/protected/${cognito-identity.amazonaws.com:sub}/*",
                "arn:aws:s3:::photos/private_resized/${cognito-identity.amazonaws.com:sub}/*",
                "arn:aws:s3:::photos/private/${cognito-identity.amazonaws.com:sub}/*"
            ],
            "Effect": "Allow"
        },

After that, it worked. Thanks for that pointer. It make more sense to put the customPrefix support in Storage.configure().

github-actions[bot] commented 3 years ago

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.