0

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.

sideshowbarker
  • 81,827
  • 26
  • 193
  • 197
cdi-meo
  • 144
  • 2
  • 3
  • First thing I'd say is don't use localhost, look here: http://stackoverflow.com/questions/10883211/deadly-cors-when-http-localhost-is-the-origin – nakulthebuilder Apr 18 '17 at 09:30

1 Answers1

3

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.

Rob Mullins
  • 611
  • 4
  • 7
  • Thanks for trying. However, the code above fails to work: https://bucketname.s3.amazonaws.com/categories/imagename.png net::ERR_TUNNEL_CONNECTION_FAILED – cdi-meo Feb 02 '17 at 12:57
  • I updated and provided a complete component for you to use -- which I have tested using my own S3 bucket. Also, provided pretty detailed instructions. – Rob Mullins Feb 08 '17 at 00:42
  • Hi Rob, I am using Quickstart from angular.io. When using this code: import { Credentials, S3 } from 'aws-sdk'; Is there something I need to configure in system.js so that the import will work? – cdi-meo Feb 08 '17 at 15:21
  • "(SystemJS) XHR error (404 Not Found) loading http://localhost:3000/aws-sdk↵ Error: XHR error (404 Not Found) loading http://localhost:3000/aws-sdk ... – cdi-meo Feb 08 '17 at 15:36
  • I added lots more information and configuration. While simple in the end, getting the systemjs stuff right took me some time... In original answer, I was using an clean angular-cli project (using webpack), which works out of the box. – Rob Mullins Feb 08 '17 at 19:36
  • I really am so thankful that you are still supporting. I am using US East (N. Virginia). As per http://docs.aws.amazon.com/general/latest/gr/rande.html, this is us-east-1. And valid endpoint (only for this region) is s3.amazonaws.com. And the aws sdk does really configure this as such. The bucketname is 100% correct. But there is an error: OPTIONS https://bucketname.s3.amazonaws.com/categories/imagename.PNG net::ERR_TUNNEL_CONNECTION_FAILED. In your log: Error Message: Network Failure – cdi-meo Feb 09 '17 at 10:46
  • We've come too far to not solve the problem. :) If your bucket is in US East, per AWS console [S3 overview](https://console.aws.amazon.com/s3/home), you are right that it is the exception, having no region in the endpoint. However, the bucketname should still come after that in the URL request that is formed (if it comes before it will not work properly). Please provide more info: (1) What version of the AWS SDK per instructions? If not 2.10.0, update with npm. (2) Use Chrome dev tools network tab and include the entire contents from response for the OPTIONS request (per instructions). – Rob Mullins Feb 09 '17 at 13:33
  • Note: I also updated the component info display to account for the endpoint exception for us-east-1. (and I created my own bucket on us-east-1 and tested again, successfully) – Rob Mullins Feb 09 '17 at 14:16
  • Thx @RobMullins, this works great and really helped me a lot to understand how to implement S3 with angular4 (v4 in my case). – Janpan May 23 '17 at 18:49