Skip to content

Commit 04733d8

Browse files
committed
initial commit
0 parents  commit 04733d8

8 files changed

Lines changed: 292 additions & 0 deletions

File tree

.github/workflows/gdrive_auth.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pickle
2+
import os
3+
from google_auth_oauthlib.flow import Flow, InstalledAppFlow
4+
from googleapiclient.discovery import build
5+
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
6+
from google.auth.transport.requests import Request
7+
8+
9+
def Create_Service(client_secret_file, api_name, api_version, *scopes):
10+
print(client_secret_file, api_name, api_version, scopes, sep='-')
11+
CLIENT_SECRET_FILE = client_secret_file
12+
API_SERVICE_NAME = api_name
13+
API_VERSION = api_version
14+
SCOPES = [scope for scope in scopes[0]]
15+
print(SCOPES)
16+
17+
cred = None
18+
19+
pickle_file = f'token_{API_SERVICE_NAME}_{API_VERSION}.pickle'
20+
21+
if os.path.exists(pickle_file):
22+
with open(pickle_file, 'rb') as token:
23+
cred = pickle.load(token)
24+
25+
if not cred or not cred.valid:
26+
if cred and cred.expired and cred.refresh_token:
27+
cred.refresh(Request())
28+
else:
29+
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES)
30+
cred = flow.run_local_server()
31+
32+
with open(pickle_file, 'wb') as token:
33+
pickle.dump(cred, token)
34+
35+
try:
36+
service = build(API_SERVICE_NAME, API_VERSION, credentials=cred)
37+
print(API_SERVICE_NAME, 'service created successfully')
38+
return service
39+
except Exception as e:
40+
print('Unable to connect.')
41+
print(e)
42+
return None
43+
44+
def convert_to_RFC_datetime(year=1900, month=1, day=1, hour=0, minute=0):
45+
dt = datetime.datetime(year, month, day, hour, minute, 0).isoformat() + 'Z'
46+
return dt
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
import pathlib
3+
from googleapiclient.errors import HttpError
4+
from googleapiclient.http import MediaFileUpload
5+
6+
7+
def get_existing_files_recursive(folder_id, service):
8+
query = f"'{folder_id}' in parents and trashed=false"
9+
files = {}
10+
folders = {}
11+
12+
def recurse(current_id, path_prefix):
13+
results = service.files().list(q=f"'{current_id}' in parents and trashed=false",
14+
fields="files(id, name, mimeType)").execute()
15+
for item in results['files']:
16+
full_path = os.path.join(path_prefix, item['name'])
17+
if item['mimeType'] == 'application/vnd.google-apps.folder':
18+
folders[full_path] = item['id']
19+
recurse(item['id'], full_path)
20+
else:
21+
files[full_path] = item['id']
22+
23+
recurse(folder_id, "")
24+
return {"files": files, "folders": folders}
25+
26+
def ensure_folder(path, parent_id, drive_service, existing_items):
27+
folders = existing_items["folders"]
28+
if path in folders:
29+
return folders[path]
30+
31+
folder_name = os.path.basename(path)
32+
parent_path = os.path.dirname(path)
33+
if parent_path != "":
34+
parent_folder_id = ensure_folder(parent_path, parent_id, drive_service, existing_items)
35+
else:
36+
parent_folder_id = parent_id
37+
38+
file_metadata = {
39+
"name": folder_name,
40+
"mimeType": "application/vnd.google-apps.folder",
41+
"parents": [parent_folder_id]
42+
}
43+
folder = drive_service.files().create(body=file_metadata, fields="id").execute()
44+
folders[path] = folder["id"]
45+
return folder["id"]
46+
47+
def upload_file_or_folder(local_base_path, parent_drive_folder_id, drive_service, existing_items):
48+
for local_path in pathlib.Path(local_base_path).rglob("*"):
49+
relative_path = str(local_path.relative_to(local_base_path))
50+
51+
if local_path.is_dir():
52+
ensure_folder(relative_path, parent_drive_folder_id, drive_service, existing_items)
53+
print(f"✅ Added folder: {relative_path}")
54+
55+
elif local_path.is_file():
56+
57+
parent_path = os.path.dirname(relative_path)
58+
if parent_path == "":
59+
parent_id = parent_drive_folder_id
60+
else:
61+
parent_id = ensure_folder(parent_path, parent_drive_folder_id, drive_service, existing_items)
62+
63+
if relative_path in existing_items["files"]:
64+
continue # Already exists, skip
65+
file_metadata = {
66+
"name": os.path.basename(local_path),
67+
"parents": [parent_id]
68+
}
69+
media = MediaFileUpload(str(local_path))
70+
file = drive_service.files().create(
71+
body=file_metadata,
72+
media_body=media,
73+
fields="id"
74+
).execute()
75+
existing_items["files"][relative_path] = file["id"]
76+
print(f"✅ Added file: {relative_path}")
77+
78+
79+
def delete_items_not_in_local(local_base_path, parent_drive_folder_id, drive_service, existing_items):
80+
local_files = set()
81+
local_folders = set()
82+
83+
for path in pathlib.Path(local_base_path).rglob("*"):
84+
rel = str(path.relative_to(local_base_path))
85+
if path.is_file():
86+
local_files.add(rel)
87+
elif path.is_dir():
88+
local_folders.add(rel)
89+
90+
for remote_path, file_id in existing_items["files"].items():
91+
if remote_path not in local_files:
92+
try:
93+
drive_service.files().delete(fileId=file_id).execute()
94+
print(f"❌ Removed file: {remote_path}")
95+
except HttpError as e:
96+
print(f"Error when removing {remote_path}: {e}")
97+
98+
# Remove folders from the deepest one
99+
for remote_path in sorted(existing_items["folders"].keys(), key=lambda x: -x.count("/")):
100+
if remote_path not in local_folders:
101+
try:
102+
drive_service.files().delete(fileId=existing_items["folders"][remote_path]).execute()
103+
print(f"❌ Removed directory: {remote_path}")
104+
except HttpError as e:
105+
print(f"Error when removing directory {remote_path}: {e}")

.github/workflows/update_drive.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
import pathlib
3+
from gdrive_auth import Create_Service
4+
from gdrive_utility import (
5+
get_existing_files_recursive,
6+
upload_file_or_folder,
7+
delete_items_not_in_local
8+
)
9+
10+
OAUTH2_SECRET = 'credentials.json'
11+
SCOPES = ['https://www.googleapis.com/auth/drive']
12+
API_TYPE = 'drive'
13+
API_VERSION = 'v3'
14+
15+
LOCAL_FOLDER = 'shared'
16+
GDRIVE_FOLDER_ID = 'YOUR_FOLDER_ID'
17+
18+
def main():
19+
if not os.path.isdir(LOCAL_FOLDER):
20+
print(f"Folder '{LOCAL_FOLDER}' not found.")
21+
return
22+
23+
drive_service = Create_Service(OAUTH2_SECRET, API_TYPE, API_VERSION, SCOPES)
24+
25+
print("📝 Getting file list from google drive...")
26+
existing_files = get_existing_files_recursive(GDRIVE_FOLDER_ID, drive_service)
27+
28+
print("⬆️ Uploading local files/folders to Google Drive...")
29+
upload_file_or_folder(
30+
local_base_path=LOCAL_FOLDER,
31+
parent_drive_folder_id=GDRIVE_FOLDER_ID,
32+
drive_service=drive_service,
33+
existing_items=existing_files
34+
)
35+
36+
print("🧹 Removing non-existent local files/directories from Google Drive...")
37+
delete_items_not_in_local(
38+
local_base_path=LOCAL_FOLDER,
39+
parent_drive_folder_id=GDRIVE_FOLDER_ID,
40+
drive_service=drive_service,
41+
existing_items=existing_files
42+
)
43+
44+
if __name__ == '__main__':
45+
main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Update Google Drive
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
upload:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: '3.11'
19+
20+
- name: Install dependencies
21+
run: pip install google-api-python-client google-auth google-auth-oauthlib google-auth-httplib2
22+
23+
- name: Restore credentials.json
24+
run: |
25+
echo "${{ secrets.GDRIVE_CREDENTIALS_B64 }}" | base64 -d > credentials.json
26+
27+
- name: Restore token_drive_v3.pickle
28+
run: |
29+
echo "${{ secrets.GDRIVE_TOKEN_B64 }}" | base64 -d > token_drive_v3.pickle
30+
31+
- name: Usuń pliki .gitkeep
32+
run: find . -name ".gitkeep" -delete
33+
34+
- name: Upload to Google Drive
35+
run: python .github/workflows/update_drive.py
36+
working-directory: ${{ github.workspace }}

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# repo credentials
2+
credentials.json
3+
credentials_b64.txt
4+
token_drive_v3.pickle
5+
token_b64.txt
6+
*.env

HowToUse.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# How to contribute?
2+
3+
## Tutorial - mixed CLI and GitHub page
4+
Quick forking tutorial for newbies: https://www.youtube.com/watch?v=CML6vfKjQss
5+
6+
## Tutorial - CLI only:
7+
```bash
8+
# install CLI GitHub and Git (gh and git packages)
9+
sudo apt install git
10+
sudo apt install gh
11+
gh auth login
12+
13+
# fork the repo
14+
gh repo fork Billypl/PaczkownicyInfaPG --clone
15+
cd PaczkownicyInfaPG/paczka
16+
git checkout -b YOUR_BRANCH_NAME
17+
18+
# add your files here
19+
git add .
20+
git commit -m "YOUR_MESSAGE_HERE"
21+
# when you want to push changes to your forked repo:
22+
# for the first time
23+
git push -u origin YOUR_BRANCHNAME
24+
# any other time
25+
git push
26+
27+
# when you want to merge changes to original repo
28+
gh pr create --base master --head YOUR_USERNAME:YOUR_BRANCH_NAME --repo Billypl/PaczkownicyInfaPG
29+
```

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Github to Google Drive Sync
2+
3+
Helpful tutorial on how to start:
4+
Getting started: https://www.youtube.com/watch?v=I5ili_1G0Vk
5+
How to intagrate drive API: https://www.youtube.com/watch?v=9K2P2bWEd90&list=PL3JVwFmb_BnTamTxXbmlwpspYdpmaHdbz
6+
Google API reference: https://developers.google.com/workspace/drive/api/reference/rest
7+
Google Cloude Console: https://console.cloud.google.com/welcome
8+
9+
## Setup
10+
1. go to https://console.cloud.google.com/welcome
11+
2. **Create project:** near GoogleCloud logo -> Create Project -> New Project
12+
3. **enable OAuth Consent Screen:** navigation menu -> APIs & Services -> OAuth consent screen - set external + provide app name and email + add scope (/auth/drive)
13+
4. **enable drive integration:** navigation menu -> APIs & Services -> Library -> Google Drive API -> Enable
14+
5. **generate credentials:** navigation menu -> APIs & Services -> Credentials -> DesktopApp -> Download JSON credentials file
15+
6. **generate authentication token:** Run the `./update_drive.py` script for the first time. It will ask you to open link to authorize with Google account. This will generate `*.pickle` file
16+
7. **encode the token file in base-64**: `base64 -w 0 token_drive_v3.pickle > token_b64.txt` command
17+
8. **encode the credentials file in base-64**: `base64 -w 0 credentials.json > credentials_b64.txt`
18+
9. **set secrects in GitHub repo**: GitHub -> Settings -> Security | Serets and variables -> Actions -> New Repository Secret -> GDRIVE_TOKEN_B64 and GDRIVE_CREDENTIALS_B64
19+
10. **get google drive folder id**: Google Drive -> Share -> Copy Link. Copy part after `/folders/`
20+
11. **set google drive folder id**: in [update_drive.py](.github/workflows/update_drive.py) change `YOUR_FOLDER_ID` to your destination google drive folder ID extracted in the previous step
21+
12. **voila!**: any file you put in [shared](shared/) folder and commit will be automatically uploaded to Google Drive
22+
23+
## Important
24+
- Empty folders on github are not commited. In order to commit empty folder, you need to create ".gitkeep" file. It will be removed automatically befor upload to Google Drive.
25+
- For shared repo usage instruction read [HowToUse.md](HowToUse.md)

shared/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)