gogoogle-cloud-platformdrive

Downloading google calendar event attachment


I've been using google calendar to track events. In short, I'd like to grab these events and download the attachments associated with them. I'm doing this in Go, but I'm open to any working solutions..

What I'm attempting to do is:

  credentials.json [Where ServiceAccount and GoogleProject are my service account and google projects.]
  {
  "type": "service_account",
  "project_id": "GoogleProject",
  "private_key_id": "...",
  "private_key": "-----BEGIN PRIVATE KEY-----...\n-----END PRIVATE KEY-----\n",
  "client_email": "ServiceAccount@GoogleProject.iam.gserviceaccount.com",
  "client_id": "...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ServiceAccount%40GoogleProject.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}

and create a google.HTTPClient

    import (
        "context"
        "flag"
        "fmt"
        "net/http"
        "net/http/httputil"
        "os"
        "os/signal"
        "syscall"
        "time"
        ...... // Other imports
        gCalendar "google.golang.org/api/calendar/v3"
        gDrive "google.golang.org/api/drive/v3"
    )

    var googleCalendarID := "..."// The ID of SOMEONES google calendar

    var googleCredentialsJson []byte
    if googleCredentialsJson, err := os.ReadFile(
        "google_application_credentials.json",
    ); err != nil || googleCredentialsJson == nil || len(googleCredentialsJson) == 0 {
        log.Errorf("failed to read google_application_credentials.json:\n%v", err)
    } 

    var googleJWTConf *jwt.Config
    if googleJWTConf, err = google.JWTConfigFromJSON([]byte("GOOGLE_APPLICATION_CREDENTIALS"),
        gCalendar.CalendarScope,
        gCalendar.CalendarEventsScope,
        gDrive.DriveScope,
        "https://www.googleapis.com/auth/cloud-platform",
    ); err != nil {
        log.Fatalf("error loading google credentials\n%+v", err)
    }

    googleCtx := context.Background()
    googleHTTPClient := googleJWTConf.Client(googleCtx)
    var googleCalendarService *gCalendar.Service
    if googleCalendarService, err = gCalendar.NewService(googleCtx, option.WithHTTPClient(googleHTTPClient)); err != nil {
        log.Fatalf("failed to start google calendar client\n%+v", err)
    }

    calEvents, err := googleCalendarService.Events.
        List(googleCalendarID).
        TimeMin(time.Now().Format(time.RFC3339)).
        TimeMax(time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339)).
        Context(googleCtx).Do()
    if err != nil {
        return nil, errors.Wrapf(err, "failed to list events for calendar %v", googleCalendarID)
    }    
    calEvent := calEvents[0] // Just grab the first one until this works..
    if len(calEvent.Attachments) > 0 {
        rFileUrl = calEvent.Attachments[0].FileUrl // Just grab the first one until this works..

        var request *http.Request
        var err error
        if request, err = http.NewRequestWithContext(ctx, "GET", rFileUrl, nil); err != nil {
            return "", errors.Wrapf(err, "error creating request for file '%v'", rFileUrl)
        }

        var response *http.Response
        if response, err = googleHTTPClient.Get(request); err != nil {
            return "", errors.Wrapf(err, "error retrieving file '%v'", rFileUrl)
        }
        defer response.Body.Close()

        if response.StatusCode != http.StatusOK || "DENY" == response.Header.Get("X-Frame-Options") {
            if tmpDump, err = httputil.DumpResponse(response, false); err != nil {
                return "", errors.Wrapf(err, "error processing response code %v", response.StatusCode)
            }

            log.Errorf("dump:\n%+v", string(tmpDump))
            return "", errors.Errorf("response code %v", response.StatusCode)
        }
    }

I've tried just about every url format I could find from my google searches..but every single attempt I am met with a redirect to google sign in. This happens even if the file is public (Interestingly, I can access it using the same url in a browser session that is not logged in to google). I can find tons of material on downloading files from your drive... but not a great deal on calendar attachments (either manually added, or added from drive). I'm not entirely clear on where files are stored when attached to a calendar event.. It seems like there's a bunch of redirects before the image is finally loaded?


Solution

  • I think your issue is that FileUrl has the format shown below which uses the browser-based Drive client and which requires browser-based auth:

    https://drive.google.com/open?id={FileId}"
    

    What you want is to use the Drive API files.get URL (with ?alt=media) which accepts the Authorization header provided by your HTTP client:

    https://www.googleapis.com/drive/v3/files/{FileId}?alt=media
    

    So, try:

    const endpoint string = "https://www.googleapis.com/drive/v3"
    rFileID = calEvent.Attachments[0].FileId
    
    rFileURL = fmt.Sprintf("%s/files/%s?alt=media", endpoint, rFileID)
    
    Update

    I'm unable to repro your issue making it difficult to help.

    I added a calendar entry to my Workspace user calendar with an attachment (image of my dog, of course) from my Drive.

    The following code works for me:

    package main
    
    import (
        "context"
        "fmt"
        "io"
        "log/slog"
        "net/http"
        "os"
        "time"
    
        "golang.org/x/oauth2/google"
        "google.golang.org/api/calendar/v3"
        "google.golang.org/api/drive/v3"
        "google.golang.org/api/option"
    )
    
    const (
        endpoint string = "https://www.googleapis.com/drive/v3"
    )
    
    func main() {
        scopes := []string{
            calendar.CalendarScope,
            drive.DriveScope,
        }
    
        key := os.Getenv("KEY")
        b, err := os.ReadFile(key)
        if err != nil {
            panic(err)
        }
    
        config, err := google.JWTConfigFromJSON(b, scopes...)
        if err != nil {
            panic(err)
        }
    
        subject := os.Getenv("EMAIL")
        config.Subject = subject
    
        ctx := context.Background()
    
        client := config.Client(ctx)
    
        cal, err := calendar.NewService(ctx, option.WithHTTPClient(client))
        if err != nil {
            panic(err)
        }
    
        // There is a calendar entry with a (Drive) attachment
        // on {subject} calendar on 01-Nov-2024
        now := time.Date(2024, 11, 1, 12, 0, 0, 0, time.Local)
    
        events, err := cal.Events
            .List(subject)
            .TimeMin(now.Add(-12 * time.Hour).Format(time.RFC3339))
            .TimeMax(now.Add( 12 * time.Hour).Format(time.RFC3339))
            .Context(ctx)
            .Do()
        if err != nil {
            panic(err)
        }
    
        for _, event := range events.Items {
            slog.Info("Event", "ID", event.Id)
            for _, attachment := range event.Attachments {
                slog.Info("Attachments",
                    "FileID", attachment.FileId,
                    "FileURL", attachment.FileUrl,
                )
    
                url := fmt.Sprintf("%s/files/%s?alt=media",
                    endpoint,
                    attachment.FileId,
                )
                slog.Info("URL", "url", url)
    
                resp, err := client.Get(url)
                if err != nil {
                    panic(err)
                }
                defer resp.Body.Close()
    
                if resp.StatusCode != http.StatusOK {
                    os.Exit(1)
                }
    
                out, err := os.Create("Freddie.jpg")
                if err != nil {
                    panic(err)
                }
                defer out.Close()
    
                _, err = io.Copy(out, resp.Body)
                if err != nil {
                    panic(err)
                }
            }
        }
    }
    

    Where:

    KEY="..."  # Domain-wide Delegate JSON Key filename
    USER="..." # Workspace user's email address
    

    You can test using Google's APIs Explorer