--- title: "クライアントディレクトリの設定" url: https://docs.catalyst.zoho.com/ja/tutorials/workdrivesync/java/configure-client-directory/ description: "Catalyst関数とEvent Listenerを使用して、Catalyst StratusとZoho WorkDrive間でAPIを通じてファイルのアップロードと削除を同期するWebアプリケーションを構築します" last_updated: 2026-08-28T07:59:38Z service: All Services type: tutorial related_links: - name: "クライアントディレクトリ構造" url: /ja/cli/v1/project-directory-structure/client-directory --- # クライアントディレクトリの設定 次に、クライアントコンポーネントを設定しましょう。[クライアントディレクトリ](https://docs.catalyst.zoho.com/ja/cli/v1/project-directory-structure/client-directory)には以下が含まれています: * フロントエンドアプリケーションのHTMLコードを含む`index.html`ファイル * CSSコードを含む`main.css`ファイル * JavaScriptコードを含む`main.js`ファイル * `client-package.json`設定ファイル `index.html`、`main.js`、`main.css`をコーディングします。 > **注意:** このセクションのコードを十分に理解するため、必ず内容を確認してください。コードについてはこのセクションの最後で説明します。 以下のコードをコピーして、IDEを使用してプロジェクトの`client/`ディレクトリにある各ファイルに貼り付けて保存してください。 **index.html** ```xml <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Catalyst File Vault</title> <script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="main.css"> </head> <body class="min-h-screen bg-gradient-to-br from-[#DCE7FF] to-blue-100 text-gray-800 flex items-center justify-center p-4"> <main class="w-full max-w-6xl"> <div class="bg-white p-6 rounded-lg shadow-2xl border border-blue-100 animate-card-fade-in-scale"> <div class="flex flex-col sm:flex-row items-center justify-between mb-6 pb-4 border-b border-gray-200"> <button id="logout-button" class="bg-transparent border-2 border-red-500 text-red-600 hover:bg-red-100 hover:text-red-700 font-semibold py-2 px-5 rounded-full shadow hover:shadow-lg transition-all duration-300 flex items-center justify-center text-sm order-2 sm:order-1 mt-4 sm:mt-0"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> <polyline points="17 17 22 12 17 7"></polyline> <line x1="22" y1="12" x2="12" y2="12"></line> </svg> LOGOUT </button> <h1 class="text-4xl sm:text-5xl font-extrabold text-blue-700 text-center flex-grow order-1 sm:order-2">Catalyst File Vault</h1> <button id="upload-button" class="bg-transparent border-2 border-green-500 text-green-600 hover:bg-green-100 hover:text-green-700 font-semibold py-2 px-5 rounded-full shadow hover:shadow-lg transition-all duration-300 flex items-center justify-center text-sm order-3 sm:order-3 mt-4 sm:mt-0"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="17 8 12 3 7 8"></polyline> <line x1="12" y1="3" x2="12" y2="15"></line> </svg> UPLOAD FILE </button> <input type="file" id="file-input" class="hidden"> </div> <div class="overflow-x-auto"> <table class="min-w-full bg-white border border-gray-200 rounded-lg overflow-hidden"> <thead> <tr class="table-header-bg text-sm uppercase tracking-wider"> <th class="py-3 px-4 text-left font-bold">File Name</th> <th class="py-3 px-4 text-left font-bold">Uploaded Time</th> <th class="py-3 px-4 text-left font-bold">File Size</th> <th class="py-3 px-4 text-left font-bold">Stratus Upload</th> <th class="py-3 px-4 text-left font-bold">WorkDrive Sync</th> <th class="py-3 px-4 text-center font-bold">Download File</th> <th class="py-3 px-4 text-center font-bold">Delete File</th> </tr> </thead> <tbody id="file-table-body" class="divide-y divide-gray-100"> <tr id="loading-row"> <td colspan="7" class="py-6 text-center"> <div class="flex flex-col items-center justify-center"> <div class="animate-spin-custom rounded-full h-10 w-10 border-t-4 border-b-4 border-[#0059E9] mb-3"> </div> <p class="text-gray-500">Loading files...</p> </div> </td> </tr> <tr id="no-files-row" class="hidden"> <td colspan="7" class="py-16 text-center bg-blue-50/50 rounded-b-lg"> <div class="flex flex-col items-center justify-center text-gray-700"> <svg xmlns="http://www.w3.org/2000/svg" class="h-20 w-20 text-blue-500 mb-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"> </path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="12" y1="18" x2="12" y2="12"></line> <line x1="9" y1="15" x2="15" y2="15"></line> </svg> <p class="text-3xl font-extrabold mb-2 text-blue-800">No files here yet!</p> <p class="text-lg text-gray-600 mt-4">Your vault is empty. Click the 'Upload File' button in the top right to add your first file!</p> </div> </td> </tr> </tbody> </table> </div> </div> </main> <div id="message-overlay" class="message-overlay hidden"> <div class="message-box"> <p id="message-text" class="text-lg font-semibold mb-4"></p> <button id="message-close-button" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Close</button> </div> </div> <script src="https://static.zohocdn.com/catalyst/sdk/js/4.6.2/catalystWebSDK.js"></script> <script src="/__catalyst/sdk/init.js"></script> <script src="main.js" defer></script> </body> </html> ``` **main.css** ```css @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .animate-spin-custom { animation: spin 1s linear infinite; } body { font-family: 'Inter', sans-serif; } .table-header-bg { background-image: linear-gradient(to right, #DBEAFE, #BFDBFE); color: #1E40AF; } #file-table-body tr:nth-child(even) { background-color: #F9FAFB; } @keyframes cardFadeInScale { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } } .animate-card-fade-in-scale { animation: cardFadeInScale 0.8s ease-out forwards; animation-delay: 0.2s; } .message-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .message-box { background-color: white; padding: 2rem; border-radius: 0.5rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: center; max-width: 400px; width: 90%; } ``` **main.js** ```javascript const bucket_name = "file-vault"; // 実際のバケット名に置き換えてください const loginURL = "https://p1-807759706.development.catalystserverless.com/__catalyst/auth/login"; // Replace your login URL document.addEventListener('DOMContentLoaded', async () => { const fileTableBody = document.getElementById('file-table-body'); const loadingRow = document.getElementById('loading-row'); const noFilesRow = document.getElementById('no-files-row'); const logoutButton = document.getElementById('logout-button'); const uploadButton = document.getElementById('upload-button'); const fileInput = document.getElementById('file-input'); const messageOverlay = document.getElementById('message-overlay'); const messageText = document.getElementById('message-text'); const messageCloseButton = document.getElementById('message-close-button'); async function checkSignInStatus() { let userIsActuallySignedIn = false; try { await catalyst.auth.isUserAuthenticated(); userIsActuallySignedIn = true; console.log("User is signed in."); } catch (err) { console.log("User not signed in:", err); userIsActuallySignedIn = false; } try { if (!userIsActuallySignedIn) { fileTableBody.innerHTML = ` <tr> <td colspan="7" class="py-6 text-center text-red-600 font-semibold"> Error: Not signed in. Redirecting to login... </td> </tr> `; window.location.href = loginURL; } } catch (error) { console.error("Failed to redirect:", error); } } function showMessage(text, isError = false) { messageText.textContent = text; if (isError) { messageText.classList.add('text-red-600'); messageText.classList.remove('text-gray-800'); } else { messageText.classList.add('text-gray-800'); messageText.classList.remove('text-red-600'); } messageOverlay.classList.remove('hidden'); } function hideMessage() { messageOverlay.classList.add('hidden'); } messageCloseButton.addEventListener('click', hideMessage); function createFileStatusBadge(status) { let bgColorClass, textColorClass, dotColorClass; switch (status) { case 'Uploaded': bgColorClass = 'bg-green-100'; textColorClass = 'text-green-800'; dotColorClass = 'text-green-400'; break; case 'Deleted': bgColorClass = 'bg-gray-200'; textColorClass = 'text-gray-700'; dotColorClass = 'text-gray-500'; break; case 'Failed': bgColorClass = 'bg-red-100'; textColorClass = 'text-red-800'; dotColorClass = 'text-red-400'; break; default: bgColorClass = 'bg-yellow-100'; textColorClass = 'text-yellow-800'; dotColorClass = 'text-yellow-400'; } return ` <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${bgColorClass} ${textColorClass}"> <svg class="-ml-0.5 mr-1.5 h-2 w-2 ${dotColorClass}" fill="currentColor" viewBox="0 0 8 8"> <circle cx="4" cy="4" r="3" /> </svg> ${status} </span> `; } function createFileRow(file) { const row = document.createElement('tr'); row.classList.add('hover:bg-blue-50', 'transition-colors', 'duration-150'); row.dataset.rowId = file.ROWID; row.dataset.fileName = file.FileName; const fileSizeFormatted = file.FileSize ? (file.FileSize / 1024).toFixed(2) + ' KB' : 'N/A'; const uploadedTimeFormatted = file.UploadedTime || 'N/A'; const isActionDisabled = file.StratusUpload === 'Deleted' || file.StratusUpload === 'Failed'; const disabledClass = isActionDisabled ? 'opacity-50 cursor-not-allowed' : ''; const downloadButtonState = isActionDisabled ? 'disabled' : ''; const deleteButtonState = isActionDisabled ? 'disabled' : ''; row.innerHTML = ` <td class="py-3 px-4 text-left text-gray-800 font-medium">${file.FileName}</td> <td class="py-3 px-4 text-left text-gray-600">${uploadedTimeFormatted}</td> <td class="py-3 px-4 text-left text-gray-600">${file.FileSize ? (file.FileSize / 1024).toFixed(2) + ' KB' : 'N/A'}</td> <td class="py-3 px-4 text-left text-gray-600">${createFileStatusBadge(file.StratusUpload)}</td> <td class="py-3 px-4 text-left text-gray-600">${createFileStatusBadge(file.WorkDriveSync)}</td> <td class="py-3 px-4 text-center"> <button class="p-2 rounded-full text-blue-600 hover:bg-blue-50 hover:text-blue-800 transition-colors duration-200 download-btn ${disabledClass}" title="Download ${file.FileName}" ${downloadButtonState}> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> </svg> </button> </td> <td class="py-3 px-4 text-center"> <button class="p-2 rounded-full text-red-600 hover:bg-red-50 hover:text-red-800 transition-colors duration-200 delete-btn ${disabledClass}" title="Delete ${file.FileName}" ${deleteButtonState}> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> </svg> </button> </td> `; return row; } logoutButton.addEventListener('click', async () => { console.log('Logging out...'); showMessage('Logging out...', false); try { await catalyst.auth.signOut(loginURL); } catch (error) { console.error('Logout failed:', error); showMessage('Logout failed. Please try again.', true); } }); uploadButton.addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', async (event) => { const file = event.target.files[0]; if (file) { showMessage(`Uploading "${file.name}"...`, false); console.log('Selected file for upload:', file.name, file.type, file.size); let uploadSuccess = false; let rowId = null; try { const table = catalyst.table.tableId('FileVault'); const details = [ { "FileName": file.name, "FileSize": file.size, "StratusUpload": "Pending", "WorkDriveSync": "Pending" } ]; const insertResponse = await table.addRow(details); rowId = insertResponse.content[0].ROWID; const bucket = catalyst.stratus.bucket(bucket_name); const putObject = await bucket.putObject(file.name, file); const putObjectResponse = await putObject.start(); if (putObjectResponse.status !== 200) { throw new Error("Something went wrong!"); } uploadSuccess = true; const date = new Date(); const options = { month: "long", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", hour12: true, }; const parts = new Intl.DateTimeFormat("en-US", options).formatToParts(date); const lookup = Object.fromEntries(parts.map(p => [p.type, p.value])); const currentTime = `${lookup.month} ${lookup.day}, ${lookup.year} ${lookup.hour}:${lookup.minute} ${lookup.dayPeriod}`; const updateDetails = [ { "UploadedTime": currentTime, "StratusUpload": "Uploaded", "ROWID": rowId } ]; await table.updateRow(updateDetails); } catch (err) { console.error('Error during upload process:', err); if (rowId) { try { const table = catalyst.table.tableId('FileVault'); const row = table.rowId(rowId); await row.delete(); } catch (updateErr) { console.error('Failed to update row status after upload error:', updateErr); } } } if (uploadSuccess) { showMessage(`"${file.name}" uploaded successfully!`, false); fetchFiles(); setTimeout(hideMessage, 1500); } else { showMessage(`Failed to upload "${file.name}". Please check console for details.`, true); setTimeout(hideMessage, 2000); } } event.target.value = null; }); async function fetchFiles() { fileTableBody.innerHTML = ''; loadingRow.classList.remove('hidden'); noFilesRow.classList.add('hidden'); fileTableBody.appendChild(loadingRow); try { const table = catalyst.table.tableId('FileVault'); const response = await table.getPagedRows({}); loadingRow.classList.add('hidden'); if (response.status === 200) { if (response.content && response.content.length > 0) { renderFiles(response.content); noFilesRow.classList.add('hidden'); } else { fileTableBody.innerHTML = ''; noFilesRow.classList.remove('hidden'); fileTableBody.appendChild(noFilesRow); } } else { fileTableBody.innerHTML = ` <tr> <td colspan="7" class="py-6 text-center text-red-600 font-semibold"> Error: Failed to load files. ${response.statusText || 'Please check your Catalyst Function.'} </td> </tr> `; console.error('Failed to fetch files:', response.statusText); } } catch (error) { loadingRow.classList.add('hidden'); fileTableBody.innerHTML = ` <tr> <td colspan="7" class="py-6 text-center text-red-600 font-semibold"> Network Error: Could not connect or retrieve data. <p class="text-sm text-gray-500 mt-2">Check console for details.</p> </td> </tr> `; console.error('Error during file fetch:', error); } } function renderFiles(files) { files.forEach(file => { fileTableBody.appendChild(createFileRow(file)); }); } async function downloadFile(fileName) { showMessage(`Preparing to download "${fileName}"...`, false); try { const bucket = catalyst.stratus.bucket(bucket_name); const getObject = await bucket.getObject(fileName); const getObjectResponse = await getObject.start(); const fileBlob = getObjectResponse.content; if (fileBlob) { const blobUrl = URL.createObjectURL(fileBlob); const a = document.createElement('a'); a.href = blobUrl; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); hideMessage(); } else { showMessage(`Failed to retrieve file content for "${fileName}".`, true); setTimeout(hideMessage, 2000); } } catch (error) { console.error('Error during file download:', error); let errorMessage = `Error downloading "${fileName}". Please try again.`; if (error && error.message && error.message.includes("404")) { errorMessage = `File "${fileName}" not found in Stratus.`; } showMessage(errorMessage, true); setTimeout(hideMessage, 3000); } } async function deleteFile(rowId, fileName) { showMessage(`Are you sure you want to delete "${fileName}"? This will remove it from storage and mark it as deleted.`, false); messageCloseButton.textContent = 'Cancel'; const confirmDeleteButton = document.createElement('button'); confirmDeleteButton.textContent = 'Delete'; confirmDeleteButton.className = 'ml-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600'; const originalCloseHandler = messageCloseButton.onclick; messageCloseButton.onclick = () => { hideMessage(); messageCloseButton.textContent = 'Close'; messageCloseButton.onclick = originalCloseHandler; confirmDeleteButton.remove(); }; messageCloseButton.parentNode.appendChild(confirmDeleteButton); confirmDeleteButton.onclick = async () => { hideMessage(); messageCloseButton.textContent = 'Close'; messageCloseButton.onclick = originalCloseHandler; confirmDeleteButton.remove(); showMessage(`Processing deletion for "${fileName}"...`, false); console.log(`Attempting to delete file from Stratus (Name: ${fileName}) and update DB (ROWID: ${rowId})`); let dbUpdateSuccess = false; let stratusDeleteSuccess = false; try { const bucket = catalyst.stratus.bucket(bucket_name); await bucket.deleteObject(fileName); stratusDeleteSuccess = true; } catch (error) { console.error('Error deleting file from Stratus:', error); showMessage(`Failed to remove "${fileName}" from storage.`, true); } try { const table = catalyst.table.tableId('FileVault'); const updateDetails = [ { "ROWID": rowId, "StratusUpload": "Deleted" } ]; await table.updateRow(updateDetails); dbUpdateSuccess = true; } catch (error) { console.error('Error updating Data Store row to "Deleted":', error); if (stratusDeleteSuccess) { showMessage(`File "${fileName}" removed from storage, but failed to mark as deleted in database.`, true); } else { showMessage(`Failed to mark "${fileName}" as deleted in database.`, true); } } if (dbUpdateSuccess && stratusDeleteSuccess) { showMessage(`"${fileName}" removed from storage!`, false); fetchFiles(); setTimeout(hideMessage, 1500); } else { fetchFiles(); setTimeout(hideMessage, 3000); } }; } fileTableBody.addEventListener('click', (event) => { const targetButton = event.target.closest('.download-btn, .delete-btn'); if (!targetButton || targetButton.disabled) return; const row = targetButton.closest('tr'); if (!row) return; const rowId = row.dataset.rowId; const fileName = row.dataset.fileName; if (targetButton.classList.contains('download-btn')) { downloadFile(fileName); } else if (targetButton.classList.contains('delete-btn')) { deleteFile(rowId, fileName); } }); await checkSignInStatus(); fetchFiles(); }); ``` 次に、以下のコードで`client-package.json`を更新します。 **client-package.json** ```json { "name": "WorkDriveSyncApp", "version": "0.0.1", "homepage" : "/__catalyst/auth/login", "login_redirect" : "index.html" } ``` クライアントディレクトリの設定が完了しました。 関数とクライアントの動作を簡単に確認しましょう: * `index.html`にはクライアントのフロントエンドのコードが含まれています。CSSは`main.css`で定義されています。 * `main.js`: クライアントコンポーネントのJavaScript関数で、以下の関数を通じてクライアントで実行されるさまざまなアクションを処理します: * **`checkSignInStatus()`:** ユーザーが認証されているかどうかを確認し、認証されている場合はアプリへのアクセスを許可し、そうでない場合はエラーを表示してログインページにリダイレクトします。 * **`fetchFiles()`:** Catalyst Data Storeテーブルからすべてのファイルレコードを取得します。 * **`uploadFile (triggered via file input)`:** Catalyst Stratusへのファイルアップロードを処理し、Data Storeのメタデータを更新します。 * **`downloadFile(fileName)`:** Stratusバケットからファイルをダウンロードします。 * **`deleteFile(rowId, fileName)`:** StratusとData Storeの両方からファイルを削除します。 * **`logout()`**: ユーザーをアプリケーションからログアウトします。 * **Event関数**は、イベントリスナーが実行されるたびにトリガーされます。この関数は以下のアクションを実行します: * ファイルストリームを使用して、イベントリスナーからイベントデータとして送信されたファイルをダウンロードします * POSTメソッドを使用したAPIを通じて、ファイルとそのメタデータをWorkDriveにアップロードします。OAuth認証情報がこのアクションの承認に渡されます。 * アクションが成功した場合、WorkDriveの同期ステータスが「Completed」に設定されます。Data StoreのレコードがWorkDriveファイルIDと同期ステータスで更新されます。