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(storage): Firebase Storage for AngularFire #865

Merged
merged 25 commits into from
Jan 12, 2017
Merged

feat(storage): Firebase Storage for AngularFire #865

merged 25 commits into from
Jan 12, 2017

Conversation

davideast
Copy link
Contributor

@davideast davideast commented Oct 18, 2016

Firebase Storage for AngularFire!

Addresses #785. This is a WIP PR, do not merge yet.

Description

This PR adds two new services to AngularFire, $firebaseStorage and the [firebase-src] directive.

API Reference

interface $firebaseStorage {
  (ref: firebase.storage.Reference): FirebaseStorage;
}

interface FirebaseStorage {
  constructor (storageRef: firebase.storage.Reference);
  $put(File, Metadata): UploadTask;
  $putString(string, format, Metadata): UploadTask;
  $getDownloadURL(): Promise<string>;
  $getMetadata(): Promise<Object>;
  $updateMetadata(): Promise<Object>;
  $delete(): Promise<void>;
  $toString(): string;
}

interface UploadTask {
  $progress: (Object) => (); 
  $error: (Error) => (); 
  $complete: (Object) => (); 
  $cancel(): boolean;
  $resume(): boolean;
  $pause(): boolean;
  $snapshot(): StorageSnapshot;
  then: Promise<Object>;
  catch: Promise<Error>;
}

Code sample

function MyCtrl($scope, $firebaseStorage) {
  const storageRef = firebase.storage().ref('thing');
  const $storage = $firebaseStorage(storageRef);
  const $task = $storage.$put(someFile);
  $task.$progress(snapshot => {

  });
  $task.$complete(snapshot => {
  
  });
}
<div class="image-holder">
   <!-- firebase-src automatically downloads from Firebase Storage -->
   <img firebase-src="thing" />
</div>

@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm.

1 similar comment
@googlebot
Copy link

We found a Contributor License Agreement for you (the sender of this pull request) and all commit authors, but as best as we can tell these commits were authored by someone else. If that's the case, please add them to this pull request and have them confirm that they're okay with these commits being contributed to Google. If we're mistaken and you did author these commits, just reply here to confirm.

@CyrusZei
Copy link

omg man, I need to test this :D

@soumak77
Copy link
Contributor

soumak77 commented Nov 8, 2016

I would like to propose creating a service similar to $firebaseRef but for storage references. I really like how $firebaseRef abstracts the exact paths to data and would like the same functionality for storage.

Currently the firebase-src directive takes a path to the data via the gs-url attribute. An alternate attribute could be added to allow passing a key for the storage reference.

// $observe is like $watch but it waits for interpolation
// Ex: <img firebase-src="{{ myUrl }}"/>
attrs.$observe('firebaseSrc', function (newVal) {
if (newVal !== '' && newVal !== null && newVal !== undefined) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand how this directive should work. It appears the directive's purpose is to generate the url to the file based on a path to the file. This path looks to be provided by the gs-url attribute. The code though is setup to watch the value myUrl passed in by the user. What exactly is the user suppose to be passing to the directive? It appears the directive should be using the observed value of the firebase-src attribute instead of the gs-url attribute.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an error from an earlier prototype. I have it fixed in a local version. I'm working on some final tests and hope to have it submitted soon!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davideast I see you made the update, but changed it to use attrs.firebaseSrc instead of the interpolated value newVal. Does that work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disregard my last comment. I just tested with the recent changes and all appears to be working.

@davideast
Copy link
Contributor Author

@soumak77 I too would like to have a provider for storage references! This release though will be dedicated to the core storage functionality. I think we could do that in the next release though 😄

@CyrusZei
Copy link

CyrusZei commented Nov 9, 2016

Oh man @davideast i am following all your updates to this firebase Storage and cant wait until I can try it out. Keep up the good work man, love it

@soumak77
Copy link
Contributor

soumak77 commented Nov 9, 2016

@davideast Could you add a firebase-href directive that works exactly the same as the firebase-src directive, except that it sets the href attribute with the file url instead of the src attribute?

The use case for such a directive would be when you want to download the file from firebase and only have a path:

<a download="" firebase-href="{{ path }}">download</a>

This mirrors the usage of firebase-src for viewing the file:

<img firebase-src="{{ path }}"></img>

@CyrusZei
Copy link

Is there by any chance that I can try this out ? and maybe add a example ? The storage feature is the only thing that I feel like I am missing. Firebase is so damn awsome.

@jwngr
Copy link

jwngr commented Dec 20, 2016

@davideast - FYI that if you merge your branch with master, the tests should properly run and pass on Travis thanks to #891.

@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this state. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@coveralls
Copy link

coveralls commented Jan 6, 2017

Coverage Status

Coverage increased (+3.7%) to 93.69% when pulling 8bd3a44 on storage into c22ef72 on master.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.7%) to 93.69% when pulling 4cdd779 on storage into c22ef72 on master.

@davideast davideast removed the request for review from katowulf January 9, 2017 12:58
@davideast davideast assigned jwngr and unassigned katowulf Jan 9, 2017
@@ -11,6 +11,9 @@
"bugs": {
"url": "https://github.com/firebase/angularfire/issues"
},
"scripts": {
"start": "grunt"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove it since we got the Travis thing figured out and in Travis, we need to run a special script anyway, not just grunt.

});
},
$error: function $error(callback) {
task.on('state_changed', function () {}, function (err) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: for clarity, use null instead of function () {}.

$progress: function $progress(callback) {
task.on('state_changed', function () {
$digestFn(function () {
callback([unwrapStorageSnapshot(task.snapshot)]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you want the brackets ([]) here. Should just be callback(unwrapStorageSnapshot(task.snapshot));. Same for the callbacks below.


function _assertStorageRef(storageRef) {
if (!isStorageRef(storageRef)) {
throw new Error('$firebaseStorage expects a storage reference from firebase.storage().ref()');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: "$firebaseStorage expects a Storage reference"

  • Capitalizing "Storage"
  • Taking out "from firebase.storage().ref()" since it could also come from "app.storage().ref()"


Storage.utils = {
_unwrapStorageSnapshot: unwrapStorageSnapshot,
_$put: _$put,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doest _$put() need to be exposed on Storage.utils? Shouldn't having Storage.$put() be enough?

expect(browser.getTitle()).toEqual('AngularFire Upload e2e Test');
});

it('starts with an empty list of messages', function () {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test seems unneeded for the upload test suite.

});

it('uploads a file', function (done) {
var fileToUpload = './upload/logo.png',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation for this whole test is off. Also, please use a new var / line for each variable.


});

describe('_$put', function () {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted above, we shouldn't need to expose this method and should just move these tests under the public $put method.

// test that $firebaseStorage.$delete() calls storageRef.delete()
var ref = firebase.storage().ref('thing');
var storage = $firebaseStorage(ref);
var fakePromise = $q(function(resolve, reject) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable.


describe('$delete', function() {
it('should call the storage ref delete method', function() {
// test that $firebaseStorage.$delete() calls storageRef.delete()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded comment.

@jwngr jwngr assigned davideast and unassigned jwngr and davideast Jan 9, 2017
@coveralls
Copy link

Coverage Status

Coverage increased (+3.7%) to 93.673% when pulling da9a148 on storage into c22ef72 on master.

@davideast davideast assigned jwngr and unassigned davideast Jan 10, 2017
Copy link

@jwngr jwngr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think this is the last major set of review comments. I took a close look at the API of the underlying Firebase SDK and think we should make a few changes to methods to better align there. Other than that, everything looks good!

(function() {
"use strict";

function unwrapStorageSnapshot(storageSnapshot) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: You have _assertStorageRef() below, but don't use a leading underscore in front of unwrapStorageSnapshot() and isStorageRef(). Let's put a leading underscore in front of each of them for consistency.

var Storage = function Storage(storageRef) {
_assertStorageRef(storageRef);
return {
$put: function $put(file) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I gave a look through the API reference for firebase.storage.Reference, and I notice there are two methods which we do not have listed here:

  • putString() - I think this method was added after you started this proposal, but we should include it since it allows for some use cases that the regular put() doesn't cover. It should be easy enough to refactor the code you wrote for $put() to implement $putString() as well.
  • toString() - I assume we don't technically need to include this since it is a synchronous method and you could always just use the underlying method on the storageRef you pass in here, but it is probably good to include a $toString() method here for completeness.

$pause: task.pause,
then: task.then,
catch: task.catch,
_task: task
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like in the underlying SDK, UploadTask has a snapshot property. Since we are returning an object which is the AngularFire version of UploadTask, we should probably have $snapshot: task.snapshot here instead of _task: task.

// $observe is like $watch but it waits for interpolation
// Ex: <img firebase-src="{{ myUrl }}"/>
attrs.$observe('firebaseSrc', function (newFirebaseSrcVal) {
if (newFirebaseSrcVal !== '' &&
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think my comment last time around was confusing. This conditional can be simplified to:

if (typeof newFirebaseSrcVal === 'string' && newFirebaseSrcVal !== '')

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if the passed value of newFirebaseSrcVal is invalid, do we want to throw an error? Seems like we are just silently failing in that case which will be hard for developers to track down.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, upon further testing and research I found out the attrs.$observe converts any value into a string. Therefore if you pass in an object, number, or bool it is automatically handled which makes the string check extra work. If you pass a null or undefined it is converted to an empty string, which is the only thing we need to check for.

var app = angular.module('upload', ['firebase.storage']);

app.controller('UploadCtrl', function Upload($scope, $firebaseStorage, $timeout) {
// Create a reference (possible create a provider)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the part of this comment within parentheses required?

$scope.upload = function() {
$scope.isUploading = true;
$scope.metadata = {bytesTransferred: 0, totalBytes: 1};
$scope.error = {};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: use null instead of {}.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we never actually use $scope.error in the HTML file. We should add a conditional <div> which shows the error if there is one.

@coveralls
Copy link

Coverage Status

Coverage increased (+3.7%) to 93.716% when pulling e584905 on storage into c22ef72 on master.

Copy link

@jwngr jwngr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with four last minor testing related comments.

task.catch();
expect(mockTask.catch).toHaveBeenCalled();
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for $snapshot as well.

});

describe('$putString', function() {
it('should call a storage ref put', function () {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/put/putString

});
});

function setupPutTests(file, mockTask, isPutString) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/file/fileOrRawString

} else {
spyOn(ref, putMethod);
}
task = storage['$' + putMethod](file);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of isPutString, let's actually pass "raw" as the second parameter after fileOrRawString.

Let's also pass some file metadata for both put() and putString() to make sure that is getting carried on as well:

var metadata = {
  contentType: 'image/jpeg',
};

if (isPutString) {
  task = storage.putString(fileOrRawString, 'raw', metadata);
} else {
  task = storage.put(fileOrRawString, metadata);
}

Then you'll need to update the tests below which check the arguments passed to put() and putString().

@coveralls
Copy link

Coverage Status

Coverage increased (+3.7%) to 93.716% when pulling ce4f392 on storage into c22ef72 on master.

@davideast davideast merged commit 9881546 into master Jan 12, 2017
@davideast davideast changed the title WIP: feat(storage): Firebase Storage for AngularFire feat(storage): Firebase Storage for AngularFire Jan 12, 2017
@jwngr jwngr deleted the storage branch January 12, 2017 17:31
@jwngr
Copy link

jwngr commented Jan 12, 2017

LGTM. Nice work!

@fallais
Copy link

fallais commented Jan 13, 2017

Does that mean we can use the storage right now ? :)

@davideast
Copy link
Contributor Author

@fallais Not yet, but soon! I'm writing up docs and then we'll do a release :)

@bpkennedy
Copy link

Yes, can we? :)

@CyrusZei
Copy link

CyrusZei commented Jan 14, 2017 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet