Google Drive 메타데이터와 파일 내용
Google Drive 커넥터의 테이블 조회는 파일 ID, 이름, MIME 유형과 크기 같은 메타데이터를 수집하는 데 사용합니다. 파일 내용이 필요하면 배치 동기화의 DownloadFile 저장 프로시저 또는 별도의 OAuth 기반 REST API 소스를 사용하는 함수를 선택합니다.
| 목적 | 사용할 경로 | 결과 |
|---|---|---|
| 파일 레코드 탐색 및 수집 | Google Drive 커넥터의 Files 테이블 등 | 파일 메타데이터 |
| 배치에서 파일 하나 추출 | Google Drive 커넥터의 DownloadFile | 데이터셋 열에 Base64로 인코딩한 파일 내용 |
| 요청 시 일반 파일 읽기 | 별도 OAuth REST API 소스를 사용하는 Python 또는 TypeScript v2 함수 | Drive API files.get의 alt=media 응답 |
| 요청 시 일반 파일 생성 또는 교체 | 같은 REST API 소스를 사용하는 함수 | 외부 Drive 파일 생성 또는 덮어쓰기 |
Files 테이블의 FileData 속성만 선택한다고 파일 다운로드 결과가 데이터셋 열로 변환되지는 않습니다. 메타데이터 동기화에서 파일 ID를 찾은 뒤 다운로드에 전달합니다. 파일 ID만 알아도 접근할 수 있는 것은 아니며 호출 사용자와 앱의 승인 범위를 모두 충족해야 합니다.
원천 탐색, 배치 동기화, 증분 수집, OAuth 2.0 인증과 테이블 내보내기는 일반 제공(GA)합니다.
Google Drive 소스 구성
Data Connection의 +New Source에서 Google Drive를 선택하고 워커와 JDBC 속성을 구성합니다.
| 속성 | 요구 수준 | 의미 | 기본값 |
|---|---|---|---|
AuthScheme | 권장 | Google Drive 인증 방식입니다. | OAuthJWT |
InitiateOAuth | 권장 | OAuth 토큰 발급과 갱신 절차입니다. | GETANDREFRESH |
OAuthClientId | 권장 | 사용자 정의 OAuth 앱의 client ID, 즉 consumer key입니다. | 미지정 |
OAuthClientSecret | 권장 | 사용자 정의 OAuth 앱의 client secret, 즉 consumer secret입니다. | 미지정 |
OAuthJWTCert | 권장 | JWT 인증서 저장소 또는 지정한 형식의 인증 정보입니다. | 미지정 |
OAuthJWTCertType | 권장 | JWT 인증서를 담은 키 저장소 유형입니다. | GOOGLEJSONBLOB |
OAuthJWTSubject | 권장 | 앱이 위임된 접근을 요청할 사용자 subject입니다. | 미지정 |
커넥터에는 accounts.google.com과 www.googleapis.com의 외부 통신을 허용합니다. 전자는 OAuth에 필요합니다. 직접 연결할 수 없는 경로에서는 에이전트 프록시 정책을 사용하고 에이전트 호스트의 접근도 확보합니다.
사용자 OAuth 인증 코드 흐름은 Foundry 워커에서 구성합니다. Foundry 콜백 URL을 외부 앱에 등록하고 소스 개요의 Authorize로 승인합니다. 승인 관리에는 기본적으로 Owner 역할이 필요하며, ConnectOnOpen=true와 함께 사용할 수 없습니다.
메타데이터 수집용 서비스 계정
서비스 계정으로 메타데이터를 수집할 때에는 다음과 같이 설정합니다.
| 속성 | 설정 |
|---|---|
AuthScheme | OAuthJWT |
InitiateOAuth | GETANDREFRESH |
OAuthJWTCertType | GOOGLEJSONBLOB |
OAuthJWTCert | 서비스 계정 JSON 문서 전체를 암호화 필드에 입력합니다. |
OAuthJWTSubject | 도메인 전체 위임을 구성한 경우가 아니면 지정하지 않습니다. |
Readonly | 메타데이터만 읽으면 true로 설정해 드라이버를 SELECT 질의로 제한합니다. |
GOOGLEJSONBLOB은 JSON 문서 자체를 받습니다. 파일 경로를 받는 GOOGLEJSON과 다릅니다. JSON 키의 큰따옴표와 private key의 이스케이프된 줄바꿈을 유지하고 인증 정보를 코드 저장소에 넣지 않습니다. 이 구성에는 OAuthClientId와 OAuthClientSecret을 사용하지 않으므로 소스 폼에서 불필요한 행을 제거합니다.
배치 동기화로 파일 추출
동기화 정의에 다음 저장 프로시저를 지정합니다. <FILE_ID>는 메타데이터에서 확인한 실제 파일 ID로 바꿉니다.
EXECUTE DownloadFile Id = '<FILE_ID>', LocalFile = ''
LocalFile을 비우면 출력 데이터셋의 FileData에 파일 내용을 반환하며 기본 형식은 Base64입니다. 하위 변환에서 바이너리로 디코딩합니다. Readonly=true는 저장 프로시저를 거부하므로 이 수집에서는 Readonly를 비우거나 false로 설정합니다.
테이블 내보내기도 지원하지만 CData 테이블 내보내기는 대상 내용을 비우지 않는 추가 모드만 지원합니다. 키 기준 갱신, 삭제와 upsert는 지원하지 않습니다. 파일 전체를 교체하려면 아래 Drive API 경로를 사용합니다.
요청 시 Drive API 호출
별도 REST API 소스에 OAuth 2.0 외부 인증 애플리케이션(Outbound application)을 연결합니다. 소스의 내보내기와 코드 가져오기를 활성화하고 Python 또는 TypeScript v2 Functions 저장소로 가져옵니다. 함수는 이 앱을 승인한 사용자를 대신해 실행합니다. 메타데이터 소스의 서비스 계정 JSON을 재사용하는 경로가 아닙니다.
이 REST 소스에는 API 대상 www.googleapis.com:443과 OAuth 대상 accounts.google.com:443, oauth2.googleapis.com:443을 허용합니다.
| 작업 | OAuth 범위 | 접근 경계 |
|---|---|---|
| 앱이 접근할 수 있는 파일 읽기 | https://www.googleapis.com/auth/drive.file | 앱에서 생성하거나 연 파일 또는 사용자가 Google Picker로 앱에 선택해 준 파일 |
| 사용자가 접근할 수 있는 모든 파일 읽기 | https://www.googleapis.com/auth/drive.readonly | 사용자 접근 가능 파일 전체. 제한된 범위입니다. |
| 새 파일 생성 또는 허용된 파일 교체 | https://www.googleapis.com/auth/drive.file | 새 파일과 앱에 접근이 허용된 기존 파일. ID만으로 편집 권한이 생기지 않습니다. |
공개 애플리케이션에서 drive.readonly를 요청하면 Google의 제한 범위 검증 요구를 충족해야 합니다.
Workshop에서 함수를 직접 호출하는 동작은 대화형 최초 승인을 시작할 수 없습니다. 최초 승인을 제공하려면 별도의 함수 기반 액션을 사용하거나, 사용자가 같은 외부 인증 애플리케이션을 미리 승인해야 합니다. 사용자 OAuth 인증 정보를 갱신할 수 있도록 serverless 실행 모드를 사용합니다.
함수 구현
각 코드의 <SOURCE_RID>는 Functions 저장소에 가져온 REST API 소스 RID로 바꾼 뒤 게시합니다. Python에서는 requests 의존성이 필요하고 TypeScript v2에서는 @palantir/functions-sources를 사용합니다. 파일 ID, 폴더 ID와 내용은 함수 인자로 전달합니다.
일반 파일 하나 읽기
files.get에 alt=media를 지정하면 일반 바이너리 파일의 바이트를 읽습니다. 임의의 파일 ID를 사용하기 전에 메타데이터와 다운로드 가능 여부를 확인합니다. Google Docs 같은 Google Workspace 고유 문서는 이 방식 대신 지원되는 형식으로 files.export를 사용해야 합니다.
함수는 바이트를 Base64 문자열로 반환합니다. Python은 소스 프록시가 있으면 세션에 적용하고 사용자의 OAuth 토큰을 헤더에 넣습니다. 두 구현 모두 리디렉션을 따라가지 않으며 성공 상태가 아닌 응답을 오류로 처리합니다.
Python 함수
import base64
import requests
from functions.api import function
from functions.sources import get_source
SOURCE_RID = "<SOURCE_RID>"
@function(sources=[SOURCE_RID])
def read_google_drive_blob(file_id: str) -> str:
source = get_source(rid=SOURCE_RID)
session = requests.Session()
proxy = source.get_https_proxy_uri()
if proxy:
session.proxies.update({"http": proxy, "https": proxy})
session.headers["Authorization"] = (
f"Bearer {source.get_session_credentials().get().access_token}"
)
response = session.get(
f"https://www.googleapis.com/drive/v3/files/{file_id}",
params={"alt": "media", "supportsAllDrives": "true"},
allow_redirects=False,
)
if not 200 <= response.status_code < 300:
raise RuntimeError(f"Google Drive returned {response.status_code}")
return base64.b64encode(response.content).decode("ascii")
TypeScript v2 함수
import { getFetch, getSource } from "@palantir/functions-sources";
const SOURCE_RID = "<SOURCE_RID>";
export const config = { sources: [SOURCE_RID] };
export default async function readGoogleDriveBlob(fileId: string): Promise<string> {
const source = await getSource({ rid: SOURCE_RID });
const credentials = await source.sessionCredentials?.get();
if (credentials?.type !== "oauth") throw new Error("Authorize the source first");
const sourceFetch = await getFetch(source);
const response = await sourceFetch(
`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`,
{
headers: { Authorization: `Bearer ${credentials.accessToken}` },
redirect: "error",
},
);
if (!response.ok) throw new Error(`Google Drive returned ${response.status}`);
return Buffer.from(await response.arrayBuffer()).toString("base64");
}
파일 생성
다음 구현은 선택한 폴더에 메타데이터를 먼저 만들고, 반환된 파일 ID에 내용을 업로드합니다. 내용은 최대 5MB인 단순 미디어 업로드를 전제로 합니다. 업로드가 실패하면 새 파일이 빈 상태로 남을 수 있으며 중단된 업로드 재시도나 최종 바이트 재조회는 수행하지 않습니다. 복구하기 어려운 데이터를 쓰기 전에는 호출 워크플로에 필요한 확인 절차를 구성합니다.
호출하면 외부 Drive에 파일을 생성하며 반환값은 새 파일 ID입니다.
Python 함수
import base64
import requests
from functions.api import function
from functions.sources import get_source
SOURCE_RID = "<SOURCE_RID>"
@function(sources=[SOURCE_RID])
def create_google_drive_blob(
parent_folder_id: str, file_name: str, mime_type: str, content_base64: str
) -> str:
source = get_source(rid=SOURCE_RID)
session = requests.Session()
proxy = source.get_https_proxy_uri()
if proxy:
session.proxies.update({"http": proxy, "https": proxy})
session.headers["Authorization"] = (
f"Bearer {source.get_session_credentials().get().access_token}"
)
created = session.post(
"https://www.googleapis.com/drive/v3/files",
json={"name": file_name, "mimeType": mime_type, "parents": [parent_folder_id]},
params={"fields": "id", "supportsAllDrives": "true"},
allow_redirects=False,
)
if not 200 <= created.status_code < 300:
raise RuntimeError(f"Google Drive returned {created.status_code}")
file_id = created.json()["id"]
uploaded = session.patch(
f"https://www.googleapis.com/upload/drive/v3/files/{file_id}",
params={"uploadType": "media", "supportsAllDrives": "true"},
data=base64.b64decode(content_base64),
headers={"Content-Type": mime_type},
allow_redirects=False,
)
if not 200 <= uploaded.status_code < 300:
raise RuntimeError(f"Google Drive returned {uploaded.status_code}")
return file_id
TypeScript v2 함수
import { getFetch, getSource } from "@palantir/functions-sources";
const SOURCE_RID = "<SOURCE_RID>";
export const config = { sources: [SOURCE_RID] };
export default async function createGoogleDriveBlob(
parentFolderId: string, fileName: string, mimeType: string, contentBase64: string,
): Promise<string> {
const source = await getSource({ rid: SOURCE_RID });
const credentials = await source.sessionCredentials?.get();
if (credentials?.type !== "oauth") throw new Error("Authorize the source first");
const sourceFetch = await getFetch(source);
const created = await sourceFetch(
"https://www.googleapis.com/drive/v3/files?fields=id&supportsAllDrives=true",
{
method: "POST",
headers: { Authorization: `Bearer ${credentials.accessToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ name: fileName, mimeType, parents: [parentFolderId] }),
redirect: "error",
},
);
if (!created.ok) throw new Error(`Google Drive returned ${created.status}`);
const { id } = (await created.json()) as { id: string };
const uploaded = await sourceFetch(
`https://www.googleapis.com/upload/drive/v3/files/${id}?uploadType=media&supportsAllDrives=true`,
{
method: "PATCH",
headers: { Authorization: `Bearer ${credentials.accessToken}`, "Content-Type": mimeType },
body: Buffer.from(contentBase64, "base64"),
redirect: "error",
},
);
if (!uploaded.ok) throw new Error(`Google Drive returned ${uploaded.status}`);
return id;
}
기존 파일 교체
다음 구현은 앱이 편집할 수 있는 파일 ID의 내용을 바로 덮어씁니다. 대상 확인, 충돌 검사와 업로드 후 재조회는 수행하지 않습니다. 교체하려는 파일인지 호출 전에 확인하고 복구할 수 있는 사본으로 시험합니다. 5MB 이하의 단순 미디어 업로드 제약은 생성 예시와 같습니다.
Python 함수
import base64
import requests
from functions.api import function
from functions.sources import get_source
SOURCE_RID = "<SOURCE_RID>"
@function(sources=[SOURCE_RID])
def replace_google_drive_blob(
file_id: str, mime_type: str, content_base64: str
) -> str:
source = get_source(rid=SOURCE_RID)
session = requests.Session()
proxy = source.get_https_proxy_uri()
if proxy:
session.proxies.update({"http": proxy, "https": proxy})
session.headers["Authorization"] = (
f"Bearer {source.get_session_credentials().get().access_token}"
)
response = session.patch(
f"https://www.googleapis.com/upload/drive/v3/files/{file_id}",
params={"uploadType": "media", "supportsAllDrives": "true"},
data=base64.b64decode(content_base64),
headers={"Content-Type": mime_type},
allow_redirects=False,
)
if not 200 <= response.status_code < 300:
raise RuntimeError(f"Google Drive returned {response.status_code}")
return file_id
TypeScript v2 함수
import { getFetch, getSource } from "@palantir/functions-sources";
const SOURCE_RID = "<SOURCE_RID>";
export const config = { sources: [SOURCE_RID] };
export default async function replaceGoogleDriveBlob(
fileId: string, mimeType: string, contentBase64: string,
): Promise<string> {
const source = await getSource({ rid: SOURCE_RID });
const credentials = await source.sessionCredentials?.get();
if (credentials?.type !== "oauth") throw new Error("Authorize the source first");
const sourceFetch = await getFetch(source);
const response = await sourceFetch(
`https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=media&supportsAllDrives=true`,
{
method: "PATCH",
headers: {
Authorization: `Bearer ${credentials.accessToken}`,
"Content-Type": mimeType,
},
body: Buffer.from(contentBase64, "base64"),
redirect: "error",
},
);
if (!response.ok) throw new Error(`Google Drive returned ${response.status}`);
return fileId;
}