diff --git a/.gitignore b/.gitignore index 127eafecb..ebf231129 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ **/.firebase **/.firebaserc **/.runtimeconfig.json +**/functions.yaml +**/.env.local */npm-debug.log lerna-debug.log *~ diff --git a/Dart/quickstarts/resize-image/.gitignore b/Dart/quickstarts/resize-image/.gitignore new file mode 100644 index 000000000..8ae1a6ef6 --- /dev/null +++ b/Dart/quickstarts/resize-image/.gitignore @@ -0,0 +1,5 @@ +.dart_tool/ +.packages +build/ +*.dart_tool +pubspec.lock diff --git a/Dart/quickstarts/resize-image/bin/server.dart b/Dart/quickstarts/resize-image/bin/server.dart new file mode 100644 index 000000000..b6e511212 --- /dev/null +++ b/Dart/quickstarts/resize-image/bin/server.dart @@ -0,0 +1,119 @@ +import 'dart:typed_data'; + +import 'package:google_cloud_storage/google_cloud_storage.dart' + show ObjectMetadata, NotFoundException; +import 'package:firebase_functions/firebase_functions.dart'; +import 'package:image/image.dart'; + +final defaultWidth = defineInt( + 'DEFAULT_WIDTH', + ParamOptions(defaultValue: 300), +); + +void main(List args) async { + await fireUp(args, (firebase) { + /// An https function that resizes images in Cloud Storage. + /// It creates a separate Storage folder to cache stored images + /// so that it does not need to resize an image twice. + /// + /// It returns an HTTP redirect to the public Storage download URL. + /// + /// The query params it accepts are: + /// - image: the image file path in Cloud Storage + /// - width (optional): the width in pixels to resize to + /// + /// Example call: https://?image=myFile.png&width=400 + firebase.https.onRequest(name: 'imageOptimizer', (request) async { + // Parse arguments from query params in the URL + final queryParams = request.url.queryParameters; + final imageFileName = queryParams['image']; + if (imageFileName == null) { + throw InvalidArgumentError( + 'No image provided. Include the image file name as a query param.', + ); + } + var width = int.tryParse(queryParams['width'] ?? ""); + if (width == null) { + logger.info( + 'Cloud not parse width from query params. Using default width.', + ); + width = defaultWidth.value(); + } + + // Get the storage bucket from the built-in parameter + // https://firebase.google.com/docs/functions/config-env#built-in-parameters + final bucketName = storageBucket.value(); + final bucket = firebase.adminApp.storage().bucket(bucketName); + + // Return early if the image has been resized before + final cachedFileName = 'image-optimizer-cache/${width}w-${imageFileName}'; + try { + await bucket.storage.objectMetadata(bucketName, cachedFileName); + final downloadUrl = await firebase.adminApp.storage().getDownloadURL( + bucket, + cachedFileName, + ); + logger.log('Cache hit. Using existing resized image.'); + return Response.movedPermanently(downloadUrl); + } on NotFoundException { + logger.log('Cache miss. Resizing image to width ${width}'); + } + + // Download original image + List originalBytes; + try { + originalBytes = await bucket.storage.downloadObject( + bucket.name, + imageFileName, + ); + } on NotFoundException { + throw InvalidArgumentError( + 'Image ${imageFileName} does not exist in bucket ${bucketName}.', + ); + } + + // Decode image + final originalImage = decodeImage(Uint8List.fromList(originalBytes)); + if (originalImage == null) { + throw InvalidArgumentError( + 'Failed to decode image. Are you sure it was an image file?', + ); + } + + // Resize if needed + var encodedBytes; + if (originalImage.width >= width) { + final resizedImage = copyResize( + originalImage, + width: width, + maintainAspect: true, + ); + encodedBytes = encodeNamedImage(imageFileName, resizedImage); + if (encodedBytes == null) { + throw InternalError('Failed to encode resized image.'); + } + } else { + logger.info( + 'Image is already smaller than the requested width. No need to resize.', + ); + encodedBytes = originalBytes; + } + + // Upload resized image to cache directory + await bucket.storage.uploadObject( + bucket.name, + cachedFileName, + encodedBytes, + // Tell clients to cache the resized image to reduce repeat requests + metadata: ObjectMetadata(cacheControl: 'public, max-age=86400'), + ); + + // Return download URL + final downloadUrl = await firebase.adminApp.storage().getDownloadURL( + bucket, + cachedFileName, + ); + return Response.movedPermanently(downloadUrl); + }); + }); +} diff --git a/Dart/quickstarts/resize-image/firebase.json b/Dart/quickstarts/resize-image/firebase.json new file mode 100644 index 000000000..adda93f17 --- /dev/null +++ b/Dart/quickstarts/resize-image/firebase.json @@ -0,0 +1,18 @@ +{ + "functions": { + "source": ".", + "codebase": "dart-quickstarts-resize-image" + }, + "emulators": { + "functions": { + "port": 5001 + }, + "storage": { + "port": 9199 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true + } +} diff --git a/Dart/quickstarts/resize-image/pubspec.yaml b/Dart/quickstarts/resize-image/pubspec.yaml new file mode 100644 index 000000000..363dac322 --- /dev/null +++ b/Dart/quickstarts/resize-image/pubspec.yaml @@ -0,0 +1,31 @@ +name: resize_image +description: Image resizer example for Firebase Functions for Dart +publish_to: none + +environment: + sdk: ^3.11.0 + +dependencies: + image: ^4.8.0 + path: ^1.9.1 + firebase_functions: + git: + url: https://github.com/firebase/firebase-functions-dart + ref: main + intl: ^0.20.2 + +dev_dependencies: + build_runner: ^2.10.5 + lints: ^6.0.0 + +dependency_overrides: + firebase_admin_sdk: + git: + url: https://github.com/firebase/firebase-admin-dart + path: packages/firebase_admin_sdk + ref: main + google_cloud_firestore: + git: + url: https://github.com/firebase/firebase-admin-dart + path: packages/google_cloud_firestore + ref: main