Skip to content

Add 'Save directly to Google Drive' functionality #431

@takaokouji

Description

@takaokouji

Summary

Add "Save directly to Google Drive" (Googleドライブに直ちに保存) menu item that allows users to quickly save changes to the currently opened Google Drive file without showing a save dialog.

Background

Currently, the application supports:

  1. Load from Google Drive: Opens a file picker to load .sb3 files
  2. Save a copy to Google Drive: Shows a dialog to save the current project as a new file or to a specific folder

However, there is no quick-save functionality to update the currently opened file. Users must use "Save a copy" each time, which requires re-entering the filename and selecting the folder.

Requirements

Functional Requirements

  1. Menu Item

    • Add "Save directly to Google Drive" (Googleドライブに直ちに保存) menu item in the File menu
    • Position it after "Save a copy to Google Drive..." menu item
    • Initially disabled (grayed out)
  2. Enabling Conditions

    • Enable when a file is loaded from Google Drive
    • Enable when a file is saved to Google Drive using "Save a copy"
    • Disable when creating a new project or loading from local file
  3. Save Behavior

    • Click "Save directly to Google Drive" to overwrite the current file in Google Drive
    • No dialog shown - save immediately
    • Use the same project data generation process as "Save a copy"
    • Display progress indicator in menu bar (same as "Save a copy")
  4. OAuth Token Management

    • OAuth tokens expire after 1 hour
    • When expired, automatically re-authenticate before saving
    • Show authentication dialog if needed
    • Resume save operation after successful re-authentication

UI/UX Requirements

Menu Structure:

File
├── New
├── Load from your computer
├── Save to your computer
├── Load from Google Drive
├── Save a copy to Google Drive...
├── Save directly to Google Drive          <-- NEW (disabled by default)
└── ...

Progress Display:

  • Show "Saving project..." in menu bar status area (same location as "Save a copy")
  • Show "Project saved." message after successful save
  • Show error message if save fails

Technical Implementation

Code Investigation Results

Based on current implementation analysis:

Current Components:

  1. GoogleDriveLoaderHOC (src/containers/google-drive-loader-hoc.jsx)

    • Handles file loading from Google Drive
    • Uses googleDriveAPI.showPicker() to select files
    • Receives file metadata: fileId, fileName
    • Currently does NOT store file metadata for later use
  2. GoogleDriveSaverHOC (src/containers/google-drive-saver-hoc.jsx)

    • Handles "Save a copy" functionality
    • State: saveStatus: 'idle' | 'saving' | 'saved'
    • Process:
      1. Convert Ruby code to blocks: this.props.targetCodeToBlocks()
      2. Generate project data: this.props.saveProjectSb3()
      3. Upload to Google Drive: googleDriveAPI.uploadFile(filename, content, folderId)
    • Shows progress in menu bar via googleDriveSaveStatus prop
  3. GoogleDriveAPI (src/lib/google-drive-api.js)

    • OAuth authentication: requestAccessToken()
    • Token validation: this.accessToken && window.gapi.client.getToken()
    • File download: downloadFile(fileId, fileName)
    • File upload (create new): uploadFile(filename, fileData, folderId) - uses POST to /upload/drive/v3/files
    • Token client: this.tokenClient (Google Identity Services)

New Implementation Requirements

  1. Redux State Management

    • Create new reducer: google-drive-file to store current file metadata
    • State structure:
      {
        fileId: string | null,        // Google Drive file ID
        fileName: string | null,       // File name with .sb3 extension
        folderId: string | null,       // Parent folder ID (null for My Drive root)
        isGoogleDriveFile: boolean     // True if current project is from Google Drive
      }
    • Actions:
      • setGoogleDriveFile(fileId, fileName, folderId) - Set current file info
      • clearGoogleDriveFile() - Clear when creating new project or loading local file
  2. Update GoogleDriveLoaderHOC

    • After successful file load, dispatch setGoogleDriveFile(fileId, fileName, null)
    • Store file metadata from picker callback
  3. Update GoogleDriveSaverHOC

    • After successful "Save a copy", dispatch setGoogleDriveFile(fileId, fileName, folderId)
    • Get file ID from upload response: response.result.id
  4. New Component: DirectGoogleDriveSaverHOC (or extend GoogleDriveSaverHOC)

    • Method: handleSaveDirectlyToGoogleDrive()
    • Process:
      1. Check if Google Drive file info exists in state
      2. Check OAuth token validity
      3. If token expired, re-authenticate: googleDriveAPI.requestAccessToken()
      4. Convert Ruby code to blocks
      5. Generate project data
      6. Update file: googleDriveAPI.updateFile(fileId, filename, content)
      7. Show progress in menu bar
    • No dialog shown - direct save operation
  5. New Method in GoogleDriveAPI: updateFile()

    /**
     * Update existing file in Google Drive
     * @param {string} fileId - Google Drive file ID
     * @param {string} filename - File name
     * @param {Blob} fileData - File content as Blob
     * @returns {Promise<object>} Update result
     */
    async updateFile(fileId, filename, fileData) {
      // Use PATCH instead of POST
      // Endpoint: /upload/drive/v3/files/{fileId}
      // Similar multipart format as uploadFile()
    }
  6. Menu Bar Integration

    • Add menu item in src/components/menu-bar/menu-bar.jsx (after line 543)
    • Props:
      • isGoogleDriveFile: boolean - Enable/disable menu item
      • onSaveDirectlyToGoogleDrive: function - Click handler
      • googleDriveSaveDirectStatus: string - Progress display ('idle' | 'saving' | 'saved')
    • Menu item:
      <MenuItem
          disabled={!this.props.isGoogleDriveFile}
          onClick={this.props.onSaveDirectlyToGoogleDrive}
      >
          <FormattedMessage
              defaultMessage="Save directly to Google Drive"
              description="Menu bar item for saving directly to currently opened Google Drive file"
              id="gui.menuBar.saveDirectlyToGoogleDrive"
          />
      </MenuItem>
  7. Internationalization

    • Add to src/locales/ja.js:
      'gui.menuBar.saveDirectlyToGoogleDrive': 'Googleドライブに直ちに保存',
      'gui.googleDriveSaver.savingDirectly': 'プロジェクトを保存中...',
      'gui.googleDriveSaver.savedDirectly': 'プロジェクトを保存しました。',
      'gui.googleDriveSaver.saveDirectError': 'Googleドライブへの保存に失敗しました。'

OAuth Token Expiration Handling

OAuth tokens expire after 1 hour. Implementation approach:

  1. Before Save Operation:

    async handleSaveDirectlyToGoogleDrive() {
        try {
            // Check token validity
            const hasValidToken = googleDriveAPI.accessToken && 
                                  window.gapi.client.getToken();
            
            if (!hasValidToken) {
                // Token expired - re-authenticate
                await googleDriveAPI.requestAccessToken();
            }
            
            // Proceed with save...
        } catch (error) {
            // Handle authentication error
        }
    }
  2. Token Client Behavior:

    • requestAccessToken() automatically shows Google OAuth consent screen if token is invalid
    • If user cancels authentication, operation should be aborted
    • If authentication succeeds, save operation resumes
  3. Error Handling:

    • Network errors during save
    • Authentication failures
    • Permission denied (file deleted or access revoked)
    • File not found (file deleted from Google Drive)

Google Drive API Reference

Update File Endpoint:

PATCH /upload/drive/v3/files/{fileId}?uploadType=multipart

Request Format:
Same multipart format as uploadFile(), but:

  • Use PATCH method instead of POST
  • Include fileId in URL path
  • Metadata can optionally update name, parents, etc.

Response:

{
  "id": "file-id",
  "name": "project-name.sb3",
  "webViewLink": "https://drive.google.com/file/d/..."
}

Implementation Checklist

Phase 1: State Management

  • Create google-drive-file reducer with actions
  • Add state to Redux store configuration
  • Create action creators and selectors

Phase 2: API Layer

  • Implement updateFile() method in google-drive-api.js
  • Add token expiration check before API calls
  • Test with expired tokens (wait 1 hour or manually invalidate)

Phase 3: Component Updates

  • Update GoogleDriveLoaderHOC to store file metadata
  • Update GoogleDriveSaverHOC to store file metadata after save-as
  • Create or extend HOC for direct save functionality
  • Add OAuth re-authentication flow

Phase 4: UI Integration

  • Add menu item to menu-bar.jsx
  • Connect menu item to Redux state (enable/disable)
  • Add progress indicator (reuse existing save status display)
  • Add internationalization messages

Phase 5: Testing

  • Manual testing: Load from Google Drive → Save directly
  • Manual testing: Save a copy → Save directly
  • Manual testing: New project → Menu item should be disabled
  • Manual testing: Wait 1 hour → Save directly → Should re-authenticate
  • Manual testing: Cancel authentication → Should not save
  • Manual testing: Delete file in Google Drive → Save directly → Should show error
  • Lint: npm run test:lint
  • Build: npm run build

Edge Cases and Error Handling

  1. File Deleted from Google Drive

    • Detect 404 error from update API
    • Show error message
    • Disable "Save directly" menu item
    • Clear Google Drive file state
  2. Permission Revoked

    • Detect 403 error from update API
    • Show error message with re-authentication option
  3. Network Disconnected

    • Detect network error
    • Show appropriate error message
    • Allow retry
  4. Multiple Browser Tabs

    • Each tab maintains its own file state
    • No synchronization needed (Google Drive handles conflicts)
  5. File Renamed in Google Drive

    • Continue using same fileId (names are for display only)
    • Update operation will succeed even if name changed externally

Related Work

Future Enhancements

  1. Auto-save

    • Periodic auto-save to Google Drive (every 2 minutes)
    • Save before closing browser tab
    • Conflict resolution
  2. File History

    • Show revision history from Google Drive
    • Revert to previous versions
  3. Keyboard Shortcut

    • Cmd+S / Ctrl+S for quick save
    • Currently saves to local computer
  4. Visual Indicator

    • Show Google Drive icon when editing a Google Drive file
    • Show "unsaved changes" indicator

Technical Reference

OAuth 2.0 Token Lifecycle

Token Duration: 1 hour (3600 seconds)

Token Structure (from Google Identity Services):

{
  access_token: "ya29.xxx...",
  expires_in: 3599,
  scope: "https://www.googleapis.com/auth/drive.file",
  token_type: "Bearer"
}

Checking Token Validity:

// Method 1: Check if token exists in gapi client
const hasToken = window.gapi.client.getToken();

// Method 2: Check stored token
const hasStoredToken = googleDriveAPI.accessToken;

// Best practice: Check both
const isTokenValid = hasStoredToken && hasToken;

Re-authentication Flow:

  1. User clicks "Save directly to Google Drive"
  2. Check token validity
  3. If invalid, call googleDriveAPI.requestAccessToken()
  4. Google shows OAuth consent screen (popup or redirect)
  5. User grants permission
  6. Token callback receives new access_token
  7. Resume save operation

Google Drive Files API v3

Update File Documentation:

Multipart Upload for Update:
Same format as create, but with PATCH method:

PATCH https://www.googleapis.com/upload/drive/v3/files/{fileId}?uploadType=multipart

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 [email protected]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions