cdi-meo
cdi-meo

Reputation: 154

Upload to AWS S3 bucket in Angular 2 project based on angular/quickstart

I need to upload from Angular 2 Quickstart base of angular.io (they do not use webpack)

  1. I used npm install aws-sdk
  2. I added this in index.html:
  3. This is the code in the component:

    @Injectable()
    export class S3Service {
    
    private uploadSuccess = true;
    
    private creds = {
        "bucket": "nameOfBucket", 
        "access_key": "accessKey",
        "secret_key": "secretKey",
        "region": "us-east-1"
    }
    
    upload(file: File){
        if (file){
            console.log('verified with file');
        }else{
            console.log('without file');
        }
    console.log('filetype verified as images/png: ', file.type);
    
    AWS.config.update({
        accessKeyId: this.creds.access_key,  
        secretAccessKey: this.creds.secret_key, 
    });
    AWS.config.region = this.creds.region;
    AWS.config.sslEnabled = false;
    
    console.log('aws.s3 is verified to be a function: ', AWS.S3);
    let bucket = new AWS.S3({ params: { Bucket: this.creds.bucket }});
    
    let key = `categories/${file.name}`;
    console.log('verified key is : ', key);
    let params = {Key: key, Body: file, ContentType: file.type, ServerSideEncryption: 'AES256'};
    
    bucket.putObject(params, function (err: Response | any, data: Response) {
        if (err){
            console.log('there is an error: ', err);
        }
        else{
            console.log('there is no error in s3 upload');
        }
    }); 
    

    }

Here is the error log in Firefox web console:

there is an error: Object { __zone_symbol__error: Error, fileName: Getter, lineNumber: Getter, columnNumber: Getter, message: Getter, stack: Getter, originalStack: Getter, zoneAwareStack: Getter, toString: createMethodProperty/props[key].value(), toSource: createMethodProperty/props[key].value(), 7 more… }

This is for Chrome:

XMLHttpRequest cannot load http://bucketname.s3.amazonaws.com/categories/imagename.png. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'localhost:3000' is therefore not allowed access. The response had HTTP status code 400.

As I am just learning, I am trying with a permissive CORS:

<CORSConfiguration xmlns="removed this from being displayed">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedOrigin>http://localhost:3000</AllowedOrigin>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <ExposeHeader>x-amz-server-side-encryption</ExposeHeader>
        <ExposeHeader>x-amz-request-id</ExposeHeader>
        <ExposeHeader>x-amz-id-2</ExposeHeader>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

The Angular 1 code successfully uploads the image.

Please help and advanced thanks.

Upvotes: 0

Views: 2584

Answers (1)

Rob Mullins
Rob Mullins

Reputation: 611

Your CORS configuration on the S3 bucket looks fine for the scenario you describe.

I believe the problem is with the endpoint URL that is resulting from your code. I had a similar problem and it was confusing and rather difficult to track down.

For some reason the AWS SDK seems to generate the endpoint URL differently depending on the method you use to set the region information and/or the bucket name. When the resulting URL doesn't contain region information (which yours does not) it causes the pre-flight request to fail, which results in a misleading error message in the brower's console about CORS (which can sometimes be the source of a preflight failure).

'Problem' endpoint format: http(s)://<bucketname>.s3.amazonaws.com/<key>
'Desired' endpoint format: http(s)://s3-<region>.amazonaws.com/<bucketname>/<key>

Try using the component I've provided here to validate your AWS S3 configuration, access and CORS settings. Then, you can easily extract out the S3 specific stuff in to a service if you choose.

Here are the steps:

  1. Confirm you are running the current version of the AWS SDK (2.10.0 as of 2017-02-08). Just in case you aren't familiar with how to check this, open /node_modules/aws-sdk/dist/aws-sdk.js and look at the comment at the top of the file to determine the version.
  2. Add the component below to your project. I have successfully tested this against my own S3 instance.
  3. Make sure you update the configuration values and specifically specify the region that your S3 bucket is in. If you are unsure you can find this using the AWS console. Of course you also need to replace the other config values using your credentials and S3 bucket name.
  4. Configure systemjs for the AWS SDK library (see system.config.js below)
  5. Add the component to your module's declarations (see app.module.ts below)
  6. Reference the component (<s3-upload-test></s3-upload-test>) in the template for AppComponent (see app.component.ts below).

s3-upload-test.component.ts:

import { Component } from '@angular/core';
import { Credentials, S3 } from 'aws-sdk';

@Component({
  selector: 's3-upload-test',
  template: `
    <div class="uploadSection">

      <hr>
      <h3>S3 File Upload Test</h3>

      <div class="subsection">
        <h4>Confirm Endpoint Format:</h4>
        <div class="indent">
          The endpoint should be in the following format <span class="monospace">s3-&lt;region&gt;.amazonaws.com</span>.
          <pre>
            Based on the configuration information you provided:
                Expect Endpoint: {{expectEndpoint}}
                Actual Endpoint: {{actualEndpoint}}
          </pre>
        </div>
      </div>

      <div class="subsection">
        <h4>Select File:</h4>
        <div class="indent">
          <input type="file" (change)="fileEvent($event)" />
        </div>
      </div>

      <div class="subsection">
        <h4>Upload Status/Results:</h4>
        <div class="indent">
          <span class="monospace result">{{uploadStatus}}</span>
        </div>
      </div>

      <hr>
    </div>
  `,
  styles: [`
    .uploadSection { font-family: sans-serif; }
    .monospace { font-family: monospace; }
    .subsection { margin-top: 35px;}
    .indent { margin-left: 20px;}
    .result { background-color: lightyellow }
  `]
})
export class S3UploadTestComponent {

  // Replace the values with your own
  private readonly _awsConfig = {
    accessKeyId: "<your keyId>",
    secretAccessKey: "<your secret>",
    s3BucketRegion: "<your region>", // example: "us-west-2"
    s3BucketName: "<your bucket>"    // example: "mycompany.testbucket"
  }
  private _awsCredentials: Credentials;
  private _s3ClientConfig: S3.ClientConfiguration;
  private _s3: S3;

  uploadStatus: string = "(no upload yet)";
  expectEndpoint: string;
  actualEndpoint: string;

  constructor() {
    // Create an AWS S3 client
    this._awsCredentials = new Credentials(this._awsConfig.accessKeyId, this._awsConfig.secretAccessKey);
    this._s3ClientConfig = {
      credentials: this._awsCredentials,
      region: this._awsConfig.s3BucketRegion,
      sslEnabled: true
    };
    this._s3 = new S3(this._s3ClientConfig);

    // Set the expected and actual endpoints
    var isRegionUSEast :boolean = (this._awsConfig.s3BucketRegion).toLowerCase() == "us-east-1";
    var endpointHost :string = isRegionUSEast ? "s3" : "s3-" + this._awsConfig.s3BucketRegion
    this.expectEndpoint = endpointHost + ".amazonaws.com";
    this.actualEndpoint = this._s3.config.endpoint;
  }

  // Event triggered when a file has been specified 
  fileEvent(fileInput: any) {
    this.uploadStatus = "starting upload...";

    // get the file to upload
    let file: File = fileInput.target.files[0];
    console.log(file);

    // upload file to S3
    let putObjectRequest: S3.PutObjectRequest = {
      Key: 'categories/' + file.name,
      Body: file,
      Bucket: this._awsConfig.s3BucketName,
      ContentType: file.type,
      ServerSideEncryption: "AES256"
    };

    // use "that" to be able to reach component properties within the then/catch callback functions
    let that = this;

    // upload to S3
    this._s3.upload(putObjectRequest).promise()
      .then(function (response: S3.ManagedUpload.SendData) {
        that.uploadStatus = "Success!\n File URI: " + response.Location;
        // alert("upload successful!");
      })
      .catch(function (err: Error) {
        var errMsg = "";
        errMsg += "upload failed.\n ";
        errMsg += "Error Message: " + err.message + "\n ";
        errMsg += "NOTE: an error message of 'Network Failure' may mean that you have the wrong region or the wrong bucket name.";
        that.uploadStatus = errMsg;
        // alert(errMsg);
      });
  }
}

systemjs.config.js additions:

(function (global) {
  System.config({
    ...
    map: {
      ...
      'aws-sdk': 'npm:aws-sdk'
    },
    packages: {
      ...
      'aws-sdk': {
        defaultExtension: 'js',
        main: 'dist/aws-sdk.js',
        format: 'global'
      }
    }
  });
})(this);

app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { S3UploadTestComponent } from './s3-upload-test.component';

@NgModule({
  imports: [BrowserModule],
  declarations: [
    AppComponent,
    S3UploadTestComponent,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Hello {{name}}</h1>
    <s3-upload-test></s3-upload-test>
  `,
})
export class AppComponent  { name = 'Angular'; }

AWS S3 bucket CORS Configuration:
NOTE: you may want to make your more restrictive, as appropriate for your security needs

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>*</AllowedHeader>
  </CORSRule>
</CORSConfiguration>

AWS IAM Policy (attach to user or group):
NOTE: you will almost certainly want to make the allowed actions more restrictive, as appropriate for your security needs
NOTE: replace <your bucketname> with appropriate value

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1485926968000",
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::<your bucketname>/*"
            ]
        }
    ]
}


If this doesn't solve your problem, use Chrome dev tools and look at the 'Network' tab to see the OPTIONS request to the S3 API and update your question with the entire response. When AWS S3 pre-flights fail, they usually provide good information in the response.

Upvotes: 3

Related Questions