code/function/function.go (133 lines of code) (raw):
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package p contains a Google Cloud Storage Cloud Function.
package p
import (
"bytes"
"context"
"fmt"
"log"
"os/exec"
"path/filepath"
"strings"
"cloud.google.com/go/storage"
)
// Global API clients used across function invocations.
var (
storageClient *storage.Client
)
func init() {
// Declare a separate err variable to avoid shadowing the client variables.
var err error
storageClient, err = storage.NewClient(context.Background())
if err != nil {
log.Fatalf("storage.NewClient: %v", err)
}
}
// GCSEvent is the payload of a GCS event. Please refer to the docs for
// additional information regarding GCS events.
type GCSEvent struct {
Bucket string `json:"bucket"`
Name string `json:"name"`
SelfLink string `json:"selfLink"`
}
// OnFileUpload prints a message when a file is changed in a Cloud Storage bucket.
func OnFileUpload(ctx context.Context, e GCSEvent) error {
log.Printf("Processing file: %s", e.Name)
tPath, oPath, err := newPaths(ctx, e)
if err != nil {
log.Printf("error: %s", err)
return err
}
if strings.Index(e.Name, "uploads/") == 0 {
if err := thumbnail(ctx, e, tPath); err != nil {
log.Printf("error: %s", err)
return err
}
if err := move(ctx, e, oPath); err != nil {
log.Printf("error: %s", err)
return err
}
if err := makePublic(ctx, e.Bucket, oPath); err != nil {
log.Printf("error: %s", err)
return err
}
if err := makePublic(ctx, e.Bucket, tPath); err != nil {
log.Printf("error: %s", err)
return err
}
}
return nil
}
func makePublic(ctx context.Context, bucket, file string) error {
obj := storageClient.Bucket(bucket).Object(file)
return obj.ACL().Set(ctx, storage.AllUsers, "READER")
}
// newPaths figures out the paths for both the original images and their
// thumbnails. It ensures that duplicate uploads will be given unique suffixes
func newPaths(ctx context.Context, e GCSEvent) (string, string, error) {
t := thumbnailPath(e.Name)
o := originalPath(e.Name)
doesExist, err := exists(ctx, e.Bucket, t)
if err != nil {
return "", "", err
}
i := 0
for doesExist {
i++
t = thumbnailPath(e.Name)
t = strings.Replace(t, "/thumbnail", fmt.Sprintf("_%d/thumbnail", i), 1)
doesExist, err = exists(ctx, e.Bucket, t)
if err != nil {
return "", "", err
}
if !doesExist {
o = originalPath(e.Name)
o = strings.Replace(o, "/original", fmt.Sprintf("_%d/original", i), 1)
}
}
return t, o, nil
}
// exists sees if a file exists already in a Cloud Storage
func exists(ctx context.Context, bucket, file string) (bool, error) {
obj := storageClient.Bucket(bucket).Object(file)
_, err := obj.Attrs(ctx)
if err == storage.ErrObjectNotExist {
return false, nil
}
if err != nil {
return false, fmt.Errorf("error checking existence of %s/%s: %s", bucket, file, err)
}
return true, nil
}
// move copies afile to the destination and deletes the original
func move(ctx context.Context, e GCSEvent, dest string) error {
src := storageClient.Bucket(e.Bucket).Object(e.Name)
dst := storageClient.Bucket(e.Bucket).Object(dest)
if _, err := dst.CopierFrom(src).Run(ctx); err != nil {
return fmt.Errorf("error copying %s to %s: %s", e.Name, dest, err)
}
if err := src.Delete(ctx); err != nil {
return fmt.Errorf("error deleting %s: %s", e.Name, err)
}
return nil
}
// thumbnail creates a smaller version of an image
func thumbnail(ctx context.Context, e GCSEvent, dest string) error {
inputBlob := storageClient.Bucket(e.Bucket).Object(e.Name)
r, err := inputBlob.NewReader(ctx)
if err != nil {
return fmt.Errorf("error in getting reading input from bucket: %v", err)
}
outputBlob := storageClient.Bucket(e.Bucket).Object(dest)
w := outputBlob.NewWriter(ctx)
defer w.Close()
// Use - as input and output to use stdin and stdout.
var stderr bytes.Buffer
cmd := exec.Command("convert", "-", "-thumbnail", "x100", "-")
cmd.Stdin = r
cmd.Stdout = w
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error in imagemagick call: %s", stderr.String())
}
return nil
}
func thumbnailPath(name string) string {
ext := filepath.Ext(name)
newBase := strings.Replace(filepath.Base(name), ext, "/thumbnail"+ext, 1)
newPath := "processed/" + newBase
return newPath
}
func originalPath(name string) string {
ext := filepath.Ext(name)
newBase := strings.Replace(filepath.Base(name), ext, "/original"+ext, 1)
newPath := "processed/" + newBase
return newPath
}