hoangthiencm commited on
Commit
8cc19f2
·
verified ·
1 Parent(s): b0ea011

Upload 4 files

Browse files
Files changed (4) hide show
  1. .gitignore +5 -0
  2. Dockerfile +19 -0
  3. package.json +21 -0
  4. server.js +260 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ uploads/
3
+ .env
4
+ *.log
5
+
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+
8
+ # Install dependencies
9
+ RUN npm install --production
10
+
11
+ # Copy source code
12
+ COPY . .
13
+
14
+ # Expose port (Hugging Face Spaces dùng port 7860)
15
+ EXPOSE 7860
16
+
17
+ # Start server
18
+ CMD ["node", "server.js"]
19
+
package.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "qlvb-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backend API để upload file lên Google Drive 2TB",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "nodemon server.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "^4.18.2",
12
+ "multer": "^1.4.5-lts.1",
13
+ "googleapis": "^126.0.0",
14
+ "cors": "^2.8.5",
15
+ "dotenv": "^16.3.1"
16
+ },
17
+ "devDependencies": {
18
+ "nodemon": "^3.0.1"
19
+ }
20
+ }
21
+
server.js ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Backend API để upload file lên Google Drive 2TB
2
+ // Sử dụng OAuth thay vì Service Account (phù hợp với tài khoản edu)
3
+ // Chạy: node server.js hoặc npm start
4
+
5
+ require('dotenv').config();
6
+ const express = require('express');
7
+ const multer = require('multer');
8
+ const { google } = require('googleapis');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const cors = require('cors');
12
+
13
+ const app = express();
14
+ const upload = multer({ dest: 'uploads/' });
15
+
16
+ // Middleware - CORS cho phép frontend từ Vercel và local
17
+ app.use(cors({
18
+ origin: process.env.ALLOWED_ORIGINS ?
19
+ process.env.ALLOWED_ORIGINS.split(',') :
20
+ '*', // Cho phép tất cả khi không set (chỉ dùng khi dev)
21
+ credentials: true
22
+ }));
23
+ app.use(express.json());
24
+
25
+ // OAuth Config - Lấy từ Google Cloud Console
26
+ const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || 'YOUR_CLIENT_ID';
27
+ const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || 'YOUR_CLIENT_SECRET';
28
+ const REDIRECT_URI = process.env.REDIRECT_URI || 'http://localhost/popup.html';
29
+
30
+ // Lưu refresh token của admin (tài khoản 2TB)
31
+ // Trong production, nên lưu vào database hoặc file bảo mật
32
+ let adminRefreshToken = null;
33
+
34
+ // Khởi tạo OAuth2 client
35
+ const oauth2Client = new google.auth.OAuth2(
36
+ GOOGLE_CLIENT_ID,
37
+ GOOGLE_CLIENT_SECRET,
38
+ REDIRECT_URI
39
+ );
40
+
41
+ // Lấy access token từ refresh token
42
+ async function getAccessToken() {
43
+ if (!adminRefreshToken) {
44
+ throw new Error('Chưa có refresh token. Vui lòng đăng nhập Admin trước.');
45
+ }
46
+
47
+ oauth2Client.setCredentials({
48
+ refresh_token: adminRefreshToken
49
+ });
50
+
51
+ const { credentials } = await oauth2Client.refreshAccessToken();
52
+ return credentials.access_token;
53
+ }
54
+
55
+ // API: Đổi OAuth code thành token
56
+ app.post('/api/oauth/token', async (req, res) => {
57
+ try {
58
+ const { code, redirect_uri } = req.body;
59
+
60
+ if (!code) {
61
+ return res.status(400).json({ error: 'Missing authorization code' });
62
+ }
63
+
64
+ const { tokens } = await oauth2Client.getToken(code);
65
+
66
+ // Lưu refresh token
67
+ if (tokens.refresh_token) {
68
+ adminRefreshToken = tokens.refresh_token;
69
+ // Trong production, nên lưu vào database
70
+ console.log('Admin refresh token đã được lưu');
71
+ }
72
+
73
+ res.json({
74
+ access_token: tokens.access_token,
75
+ refresh_token: tokens.refresh_token,
76
+ expires_in: tokens.expiry_date
77
+ });
78
+ } catch (error) {
79
+ console.error('OAuth token error:', error);
80
+ res.status(500).json({ error: error.message });
81
+ }
82
+ });
83
+
84
+ // API: Set refresh token trực tiếp (nếu đã có)
85
+ app.post('/api/oauth/set-token', async (req, res) => {
86
+ try {
87
+ const { refresh_token } = req.body;
88
+
89
+ if (!refresh_token) {
90
+ return res.status(400).json({ error: 'Missing refresh_token' });
91
+ }
92
+
93
+ adminRefreshToken = refresh_token;
94
+ res.json({ success: true, message: 'Refresh token đã được lưu' });
95
+ } catch (error) {
96
+ console.error('Set token error:', error);
97
+ res.status(500).json({ error: error.message });
98
+ }
99
+ });
100
+
101
+ // Tìm hoặc tạo folder
102
+ async function findOrCreateFolder(name, parentId = 'root') {
103
+ try {
104
+ const accessToken = await getAccessToken();
105
+ const drive = google.drive({ version: 'v3', auth: oauth2Client });
106
+
107
+ const response = await drive.files.list({
108
+ q: `mimeType='application/vnd.google-apps.folder' and name='${name}' and '${parentId}' in parents and trashed=false`,
109
+ fields: 'files(id, name)',
110
+ });
111
+
112
+ if (response.data.files.length > 0) {
113
+ return response.data.files[0];
114
+ }
115
+
116
+ // Tạo folder mới
117
+ const folder = await drive.files.create({
118
+ requestBody: {
119
+ name: name,
120
+ mimeType: 'application/vnd.google-apps.folder',
121
+ parents: [parentId],
122
+ },
123
+ fields: 'id, name',
124
+ });
125
+
126
+ return folder.data;
127
+ } catch (error) {
128
+ console.error('Error in findOrCreateFolder:', error);
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ // Upload file
134
+ async function uploadFile(filePath, fileName, parentId) {
135
+ try {
136
+ const accessToken = await getAccessToken();
137
+ const drive = google.drive({ version: 'v3', auth: oauth2Client });
138
+
139
+ const fileMetadata = {
140
+ name: fileName,
141
+ parents: [parentId],
142
+ };
143
+
144
+ const media = {
145
+ mimeType: 'application/octet-stream',
146
+ body: fs.createReadStream(filePath),
147
+ };
148
+
149
+ const file = await drive.files.create({
150
+ requestBody: fileMetadata,
151
+ media: media,
152
+ fields: 'id, name, webViewLink, webContentLink, iconLink',
153
+ });
154
+
155
+ return file.data;
156
+ } catch (error) {
157
+ console.error('Error uploading file:', error);
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ // API: Upload files
163
+ app.post('/api/upload', upload.array('files'), async (req, res) => {
164
+ try {
165
+ const { docName, type, month } = req.body; // type: 'incoming' hoặc 'outgoing'
166
+
167
+ if (!req.files || req.files.length === 0) {
168
+ return res.status(400).json({ error: 'No files uploaded' });
169
+ }
170
+
171
+ if (!adminRefreshToken) {
172
+ return res.status(401).json({ error: 'Chưa đăng nhập Admin. Vui lòng đăng nhập tài khoản Google 2TB trước.' });
173
+ }
174
+
175
+ // Tạo cấu trúc folder
176
+ const qlvbFolder = await findOrCreateFolder('QLVB-DATA');
177
+ const typeFolder = await findOrCreateFolder(
178
+ type === 'incoming' ? 'VanBanDen' : 'VanBanDi',
179
+ qlvbFolder.id
180
+ );
181
+ const monthFolder = await findOrCreateFolder(month || `Tháng ${String(new Date().getMonth() + 1).padStart(2, '0')}`, typeFolder.id);
182
+ const docFolder = await findOrCreateFolder(
183
+ `${docName} - ${Date.now()}`,
184
+ monthFolder.id
185
+ );
186
+
187
+ // Upload tất cả files
188
+ const uploadedFiles = [];
189
+ for (const file of req.files) {
190
+ const uploadedFile = await uploadFile(
191
+ file.path,
192
+ file.originalname,
193
+ docFolder.id
194
+ );
195
+ uploadedFiles.push({
196
+ id: uploadedFile.id,
197
+ name: uploadedFile.name,
198
+ iconLink: `https://drive.google.com/file/d/${uploadedFile.id}/view`,
199
+ webViewLink: uploadedFile.webViewLink,
200
+ webContentLink: uploadedFile.webContentLink,
201
+ });
202
+
203
+ // Xóa file tạm
204
+ fs.unlinkSync(file.path);
205
+ }
206
+
207
+ // Lấy danh sách files trong folder
208
+ const drive = google.drive({ version: 'v3', auth: oauth2Client });
209
+ const filesList = await drive.files.list({
210
+ q: `'${docFolder.id}' in parents and trashed=false`,
211
+ fields: 'files(id, name, iconLink, webViewLink, webContentLink)',
212
+ });
213
+
214
+ res.json({
215
+ folderId: docFolder.id,
216
+ folderUrl: `https://drive.google.com/drive/folders/${docFolder.id}`,
217
+ files: filesList.data.files || [],
218
+ });
219
+ } catch (error) {
220
+ console.error('Upload error:', error);
221
+ res.status(500).json({ error: error.message });
222
+ }
223
+ });
224
+
225
+ // API: Download file từ Google Drive (để tóm tắt)
226
+ app.get('/api/download/:fileId', async (req, res) => {
227
+ try {
228
+ const { fileId } = req.params;
229
+
230
+ if (!adminRefreshToken) {
231
+ return res.status(401).json({ error: 'Chưa đăng nhập Admin' });
232
+ }
233
+
234
+ await getAccessToken();
235
+ const drive = google.drive({ version: 'v3', auth: oauth2Client });
236
+
237
+ const response = await drive.files.get(
238
+ { fileId, alt: 'media' },
239
+ { responseType: 'stream' }
240
+ );
241
+ response.data.pipe(res);
242
+ } catch (error) {
243
+ console.error('Download error:', error);
244
+ res.status(500).json({ error: error.message });
245
+ }
246
+ });
247
+
248
+ // API: Kiểm tra trạng thái đăng nhập
249
+ app.get('/api/oauth/status', (req, res) => {
250
+ res.json({
251
+ logged_in: !!adminRefreshToken
252
+ });
253
+ });
254
+
255
+ // Hugging Face Spaces dùng port 7860, local dùng 3000
256
+ const PORT = process.env.PORT || process.env.SPACE_PORT || 3000;
257
+ app.listen(PORT, () => {
258
+ console.log(`Backend server running on port ${PORT}`);
259
+ console.log('Lưu ý: Cần đăng nhập Admin (tài khoản 2TB) trước khi upload file');
260
+ });