Skip to content

Commit 0d799da

Browse files
jamesdanielsAdmin AppleAckerApple
authored
feat(storage): getDownloadURL pipe (#2648)
Co-authored-by: Admin Apple <[email protected]> Co-authored-by: Admin Apple <[email protected]>
1 parent 1bbd3e4 commit 0d799da

File tree

6 files changed

+158
-91
lines changed

6 files changed

+158
-91
lines changed

docs/storage/storage.md

+10
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ export class AppComponent {
186186

187187
### Downloading Files
188188

189+
A convenient pipe exists for simple in page references.
190+
191+
```ts
192+
@Component({
193+
selector: 'app-root',
194+
template: `<img [src]="'users/davideast.jpg' | getDownloadURL" />`
195+
})
196+
export class AppComponent {}
197+
```
198+
189199
To download a file you'll need to create a reference and call the `getDownloadURL()` method on an `AngularFireStorageReference`.
190200

191201
```ts

sample/src/app/storage/storage.component.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ const TRANSPARENT_PNG
1313
template: `
1414
<p>
1515
Storage!
16-
<img [src]="downloadUrl$ | async" width="64" height="64"/>
16+
<img [src]="downloadUrl$ | async" width="64" height="64" />
17+
<br><small>{{ 'google-g.png' | getDownloadURL | json }}</small>
1718
</p>
1819
`,
1920
styles: []

src/storage/pipes/storageUrl.pipe.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { AsyncPipe } from '@angular/common';
2+
import { ChangeDetectorRef, NgModule, OnDestroy, Pipe, PipeTransform } from '@angular/core';
3+
import { Observable } from 'rxjs';
4+
import { AngularFireStorage } from '../storage';
5+
6+
/** to be used with in combination with | async */
7+
@Pipe({
8+
name: 'getDownloadURL',
9+
pure: false,
10+
})
11+
export class GetDownloadURLPipe implements PipeTransform, OnDestroy {
12+
13+
private asyncPipe: AsyncPipe;
14+
private path: string;
15+
private downloadUrl$: Observable<any>;
16+
17+
constructor(private storage: AngularFireStorage, cdr: ChangeDetectorRef) {
18+
this.asyncPipe = new AsyncPipe(cdr);
19+
}
20+
21+
transform(path: string) {
22+
if (path !== this.path) {
23+
this.path = path;
24+
this.downloadUrl$ = this.storage.ref(path).getDownloadURL();
25+
}
26+
return this.asyncPipe.transform(this.downloadUrl$);
27+
}
28+
29+
ngOnDestroy() {
30+
this.asyncPipe.ngOnDestroy();
31+
}
32+
33+
}
34+
35+
@NgModule({
36+
declarations: [ GetDownloadURLPipe ],
37+
exports: [ GetDownloadURLPipe ],
38+
})
39+
export class GetDownloadURLPipeModule {}

src/storage/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './storage';
33
export * from './task';
44
export * from './observable/fromTask';
55
export * from './storage.module';
6+
export * from './pipes/storageUrl.pipe';

src/storage/storage.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { NgModule } from '@angular/core';
2+
import { GetDownloadURLPipeModule } from './pipes/storageUrl.pipe';
23
import { AngularFireStorage } from './storage';
34

45
@NgModule({
6+
exports: [ GetDownloadURLPipeModule ],
57
providers: [ AngularFireStorage ]
68
})
79
export class AngularFireStorageModule { }

src/storage/storage.spec.ts

+104-90
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,45 @@
1-
import { forkJoin } from 'rxjs';
1+
import { forkJoin, from } from 'rxjs';
22
import { mergeMap, tap } from 'rxjs/operators';
33
import { TestBed } from '@angular/core/testing';
44
import { AngularFireModule, FIREBASE_APP_NAME, FIREBASE_OPTIONS, FirebaseApp } from '@angular/fire';
55
import { AngularFireStorage, AngularFireStorageModule, AngularFireUploadTask, BUCKET } from './public_api';
66
import { COMMON_CONFIG } from '../test-config';
7-
import 'firebase/storage';
87
import { rando } from '../firestore/utils.spec';
8+
import { GetDownloadURLPipe } from './pipes/storageUrl.pipe';
9+
import { ChangeDetectorRef } from '@angular/core';
10+
import 'firebase/storage';
11+
12+
if (typeof XMLHttpRequest === 'undefined') {
13+
globalThis.XMLHttpRequest = require('xhr2');
14+
}
15+
16+
const blobOrBuffer = (data: string, options: {}) => {
17+
if (typeof Blob === 'undefined') {
18+
return Buffer.from(data, 'utf8');
19+
} else {
20+
return new Blob([JSON.stringify(data)], options);
21+
}
22+
};
923

1024
describe('AngularFireStorage', () => {
1125
let app: FirebaseApp;
1226
let afStorage: AngularFireStorage;
27+
let cdr: ChangeDetectorRef;
1328

1429
beforeEach(() => {
1530
TestBed.configureTestingModule({
1631
imports: [
1732
AngularFireModule.initializeApp(COMMON_CONFIG, rando()),
18-
AngularFireStorageModule
33+
AngularFireStorageModule,
34+
],
35+
providers: [
36+
ChangeDetectorRef
1937
]
2038
});
2139

2240
app = TestBed.inject(FirebaseApp);
2341
afStorage = TestBed.inject(AngularFireStorage);
42+
cdr = TestBed.inject(ChangeDetectorRef);
2443
});
2544

2645
afterEach(() => {
@@ -39,101 +58,96 @@ describe('AngularFireStorage', () => {
3958
expect(afStorage.storage.app).toBeDefined();
4059
});
4160

42-
// TODO tests for node?
43-
if (typeof Blob !== 'undefined') {
44-
45-
describe('upload task', () => {
46-
47-
it('should upload and delete a file', (done) => {
48-
const data = { angular: 'fire' };
49-
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
50-
const ref = afStorage.ref('af.json');
51-
const task = ref.put(blob);
52-
task.snapshotChanges()
53-
.subscribe(
54-
snap => {
55-
expect(snap).toBeDefined();
56-
},
57-
done.fail,
58-
() => {
59-
ref.delete().subscribe(done, done.fail);
60-
});
61-
});
62-
63-
it('should upload a file and observe the download url', (done) => {
64-
const data = { angular: 'fire' };
65-
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
66-
const ref = afStorage.ref('af.json');
67-
ref.put(blob).then(() => {
68-
const url$ = ref.getDownloadURL();
69-
url$.subscribe(
70-
url => {
71-
expect(url).toBeDefined();
72-
},
73-
done.fail,
74-
() => {
75-
ref.delete().subscribe(done, done.fail);
76-
}
77-
);
78-
});
79-
});
61+
describe('upload task', () => {
62+
63+
it('should upload and delete a file', (done) => {
64+
const data = { angular: 'fire' };
65+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
66+
const ref = afStorage.ref('af.json');
67+
const task = ref.put(blob);
68+
task.snapshotChanges()
69+
.subscribe(
70+
snap => {
71+
expect(snap).toBeDefined();
72+
},
73+
done.fail,
74+
() => {
75+
ref.delete().subscribe(done, done.fail);
76+
});
77+
});
8078

81-
it('should resolve the task as a promise', (done) => {
82-
const data = { angular: 'promise' };
83-
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
84-
const ref = afStorage.ref('af.json');
85-
const task: AngularFireUploadTask = ref.put(blob);
86-
task.then(snap => {
87-
expect(snap).toBeDefined();
88-
done();
89-
}).catch(done.fail);
79+
it('should upload a file and observe the download url', (done) => {
80+
const data = { angular: 'fire' };
81+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
82+
const ref = afStorage.ref('af.json');
83+
ref.put(blob).then(() => {
84+
const url$ = ref.getDownloadURL();
85+
url$.subscribe(
86+
url => {
87+
expect(url).toBeDefined();
88+
},
89+
done.fail,
90+
() => {
91+
ref.delete().subscribe(done, done.fail);
92+
}
93+
);
9094
});
91-
9295
});
9396

94-
describe('reference', () => {
97+
it('should resolve the task as a promise', (done) => {
98+
const data = { angular: 'promise' };
99+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
100+
const ref = afStorage.ref('af.json');
101+
const task: AngularFireUploadTask = ref.put(blob);
102+
task.then(snap => {
103+
expect(snap).toBeDefined();
104+
done();
105+
}).catch(done.fail);
106+
});
95107

96-
it('it should upload, download, and delete', (done) => {
97-
const data = { angular: 'fire' };
98-
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
99-
const ref = afStorage.ref('af.json');
100-
const task = ref.put(blob);
101-
// Wait for the upload
102-
forkJoin([task.snapshotChanges()])
103-
.pipe(
104-
// get the url download
105-
mergeMap(() => ref.getDownloadURL()),
106-
// assert the URL
107-
tap(url => expect(url).toBeDefined()),
108-
// Delete the file
109-
mergeMap(() => ref.delete())
110-
)
111-
// finish the test
112-
.subscribe(done, done.fail);
113-
});
108+
});
114109

115-
it('should upload, get metadata, and delete', (done) => {
116-
const data = { angular: 'fire' };
117-
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
118-
const ref = afStorage.ref('af.json');
119-
const task = ref.put(blob, { customMetadata: { blah: 'blah' } });
120-
// Wait for the upload
121-
forkJoin([task.snapshotChanges()])
122-
.pipe(
123-
// get the metadata download
124-
mergeMap(() => ref.getMetadata()),
125-
// assert the URL
126-
tap(meta => expect(meta.customMetadata).toEqual({ blah: 'blah' })),
127-
// Delete the file
128-
mergeMap(() => ref.delete())
129-
)
130-
// finish the test
131-
.subscribe(done, done.fail);
132-
});
110+
describe('reference', () => {
111+
112+
it('it should upload, download, and delete', (done) => {
113+
const data = { angular: 'fire' };
114+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
115+
const ref = afStorage.ref('af.json');
116+
const task = ref.put(blob);
117+
// Wait for the upload
118+
forkJoin([task.snapshotChanges()])
119+
.pipe(
120+
// get the url download
121+
mergeMap(() => ref.getDownloadURL()),
122+
// assert the URL
123+
tap(url => expect(url).toBeDefined()),
124+
// Delete the file
125+
mergeMap(() => ref.delete())
126+
)
127+
// finish the test
128+
.subscribe(done, done.fail);
129+
});
133130

131+
it('should upload, get metadata, and delete', (done) => {
132+
const data = { angular: 'fire' };
133+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
134+
const ref = afStorage.ref('af.json');
135+
const task = ref.put(blob, { customMetadata: { blah: 'blah' } });
136+
// Wait for the upload
137+
forkJoin([task.snapshotChanges()])
138+
.pipe(
139+
// get the metadata download
140+
mergeMap(() => ref.getMetadata()),
141+
// assert the URL
142+
tap(meta => expect(meta.customMetadata).toEqual({ blah: 'blah' })),
143+
// Delete the file
144+
mergeMap(() => ref.delete())
145+
)
146+
// finish the test
147+
.subscribe(done, done.fail);
134148
});
135149

136-
}
150+
});
137151

138152
});
139153

@@ -193,7 +207,7 @@ describe('AngularFireStorage w/options', () => {
193207

194208
it('it should upload, download, and delete', (done) => {
195209
const data = { angular: 'fire' };
196-
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
210+
const blob = blobOrBuffer(JSON.stringify(data), { type: 'application/json' });
197211
const ref = afStorage.ref('af.json');
198212
const task = ref.put(blob);
199213
// Wait for the upload

0 commit comments

Comments
 (0)