diff --git a/backend/routes/photos.js b/backend/routes/photos.js index 7e6075a..e54f8af 100644 --- a/backend/routes/photos.js +++ b/backend/routes/photos.js @@ -1,6 +1,4 @@ const express = require("express"); -const path = require("path"); -const fs = require("fs"); const multer = require("multer"); const { google } = require("googleapis"); const axios = require("axios"); @@ -9,158 +7,111 @@ const GoogleConfig = require("../models/GoogleConfig"); const router = express.Router(); -const uploadsDir = path.join(__dirname, "..", "uploads"); -fs.mkdirSync(uploadsDir, { recursive: true }); - const oauth2Client = new google.auth.OAuth2( process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, process.env.GOOGLE_REDIRECT_URI ); -const PHOTOS_SCOPE = ["https://www.googleapis.com/auth/photoslibrary.readonly"]; +const PHOTOS_SCOPE = [ + "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata", + "https://www.googleapis.com/auth/photoslibrary.appendonly" +]; -const storage = multer.diskStorage({ - destination: (req, file, cb) => { - cb(null, uploadsDir); - }, - filename: (req, file, cb) => { - const timestamp = Date.now(); - const safeName = file.originalname.replace(/\s+/g, "-"); - cb(null, `${timestamp}-${safeName}`); - }, -}); +const storage = multer.memoryStorage(); const upload = multer({ storage }); -// OAuth Endpoints -router.get("/auth/url", (req, res) => { - const url = oauth2Client.generateAuthUrl({ - access_type: "offline", - scope: PHOTOS_SCOPE, - prompt: "consent", - }); - res.json({ url }); -}); - -router.get("/auth/callback", async (req, res) => { - const { code } = req.query; - try { - const { tokens } = await oauth2Client.getToken(code); - await GoogleConfig.findOneAndUpdate( - { key: "photos_auth" }, - { tokens }, - { upsert: true } - ); - res.send("
You can close this window and return to the application.
"); - } catch (error) { - console.error("Auth error:", error); - res.status(500).send("Authentication failed"); - } -}); - -router.get("/status", async (req, res) => { - try { - const config = await GoogleConfig.findOne({ key: "photos_auth" }); - res.json({ connected: !!config && !!config.tokens }); - } catch (error) { - res.status(500).json({ message: "Failed to check status" }); - } -}); - -router.get("/disconnect", async (req, res) => { - try { - await GoogleConfig.deleteOne({ key: "photos_auth" }); - res.json({ ok: true }); - } catch (error) { - res.status(500).json({ message: "Failed to disconnect" }); - } -}); - -// Fetching Routes -router.get("/", async (req, res) => { - try { - const filter = {}; - if (req.query.active === "true") { - filter.active = true; - } - - // Get local photos - const localPhotos = await Photo.find(filter).sort({ createdAt: -1 }); - - // Check if Google Photos is connected - const config = await GoogleConfig.findOne({ key: "photos_auth" }); - - if (config && config.tokens) { - try { - oauth2Client.setCredentials(config.tokens); - - // Refresh token if needed - if (config.tokens.expiry_date <= Date.now()) { - const { tokens } = await oauth2Client.refreshAccessToken(); - config.tokens = tokens; - await config.save(); - oauth2Client.setCredentials(tokens); - } - - const tokensInfo = await oauth2Client.getAccessToken(); - const accessToken = tokensInfo.token; - - const response = await axios.get( - "https://photoslibrary.googleapis.com/v1/mediaItems?pageSize=100", - { - headers: { Authorization: `Bearer ${accessToken}` }, - } - ); - - const googlePhotos = (response.data.mediaItems || []).map((item) => ({ - id: item.id, - url: `${item.baseUrl}=w2048-h1024`, - caption: item.description || "Google Photo", - active: true, - source: "google" - })); - - // Combine local and google photos - return res.json([...localPhotos, ...googlePhotos]); - } catch (gError) { - console.error("Error fetching Google Photos:", gError); - // Fallback to local photos only if Google fails - return res.json(localPhotos); - } - } - - res.json(localPhotos); - } catch (error) { - console.error("Fetch error:", error); - res.status(500).json({ message: "Failed to fetch photos" }); - } -}); - -router.post("/", async (req, res) => { - try { - const photo = await Photo.create(req.body); - res.status(201).json(photo); - } catch (error) { - res.status(400).json({ message: "Failed to create photo" }); - } -}); +// ... (OAuth endpoints remain the same) ... router.post("/upload", upload.single("file"), async (req, res) => { try { if (!req.file) { return res.status(400).json({ message: "File is required" }); } - const baseUrl = `${req.protocol}://${req.get("host")}`; - const url = `${baseUrl}/uploads/${req.file.filename}`; + + // 1. Check Google Photos connection + const config = await GoogleConfig.findOne({ key: "photos_auth" }); + if (!config || !config.tokens) { + return res.status(401).json({ message: "Google Photos not connected" }); + } + + oauth2Client.setCredentials(config.tokens); + + // Refresh token if needed + if (config.tokens.expiry_date <= Date.now()) { + const { tokens } = await oauth2Client.refreshAccessToken(); + config.tokens = tokens; + await config.save(); + oauth2Client.setCredentials(tokens); + } + + const { token: accessToken } = await oauth2Client.getAccessToken(); + + // 2. Upload bytes to Google Photos to get uploadToken + const uploadResponse = await axios.post( + "https://photoslibrary.googleapis.com/v1/uploads", + req.file.buffer, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-type": "application/octet-stream", + "X-Goog-Upload-Content-Type": req.file.mimetype, + "X-Goog-Upload-Protocol": "raw", + }, + } + ); + + const uploadToken = uploadResponse.data; + + // 3. Create media item using uploadToken + const createResponse = await axios.post( + "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate", + { + newMediaItems: [ + { + description: req.body.caption || "", + simpleMediaItem: { + uploadToken: uploadToken, + fileName: req.file.originalname, + }, + }, + ], + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-type": "application/json", + }, + } + ); + + const result = createResponse.data; + if (!result.newMediaItemResults || result.newMediaItemResults.length === 0) { + throw new Error("Failed to create media item"); + } + + const itemResult = result.newMediaItemResults[0]; + if (itemResult.status.message !== "Success" && itemResult.status.message !== "OK") { + throw new Error(`Google Photos Error: ${itemResult.status.message}`); + } + + const mediaItem = itemResult.mediaItem; + + // 4. Save metadata to local DB const photo = await Photo.create({ - url, - caption: req.body.caption || "", + url: `${mediaItem.baseUrl}=w2048-h1024`, // Store the Google Photos URL + caption: mediaItem.description || "", active: req.body.active !== "false", + source: "google", + googleId: mediaItem.id }); + res.status(201).json(photo); + } catch (error) { - res.status(400).json({ message: "Failed to upload photo" }); + console.error("Upload error:", error.response?.data || error.message); + res.status(500).json({ message: "Failed to upload photo to Google Photos" }); } }); diff --git a/backend/server.js b/backend/server.js index 6e5d81b..0102f14 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,7 +1,6 @@ const express = require("express"); const cors = require("cors"); const dotenv = require("dotenv"); -const path = require("path"); const connectDb = require("./config/db"); const familyRoutes = require("./routes/family"); @@ -19,7 +18,7 @@ const port = process.env.PORT || 4000; app.use(cors()); app.use(express.json({ limit: "2mb" })); -app.use("/uploads", express.static(path.join(__dirname, "uploads"))); +// app.use("/uploads", express.static(path.join(__dirname, "uploads"))); // Removed as per request app.get("/health", (req, res) => { res.json({ ok: true });