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?
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)
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
events.get
files.get