跳至主要内容

T7_0609

這次依然比較少: 做的是圖片儲存的部分 在登入完後,需要使用使用者本身的cookie來進行將圖片儲存至firebase storage

Spring Boot Meets Firebase: My Journey of Building a File Upload System🚀

FileStorageController

@RestController
@RequestMapping("/api/files")
public class FileStorageController {

@Autowired
private FileStorageService fileStorageService;

@PostMapping("/upload")
public ResponseEntity<String> uploadFile(@RequestParam MultipartFile file,
@CookieValue(value = "id_token", required = false) String idTokenCookie) {
CookieAuthUtil.verifyIdToken(idTokenCookie);
try {
// 3. 驗證並解碼 ID Token
FirebaseToken decoded = FirebaseAuth.getInstance()
.verifyIdToken(idTokenCookie);

String uid = decoded.getUid();
// String email = decoded.getEmail();
// String name = decoded.getName();
// String head = decoded.getPicture();
String fileId = fileStorageService.uploadFile(file, uid);
String successJson = "{\"fileId\":\"" + fileId + "\"}";
return ResponseEntity.ok(successJson);

} catch (Exception e) {
// 4. 回 500 並回錯誤訊息
// 先把雙引號 escape,避免破壞 JSON
String escaped = e.getMessage() == null
? ""
: e.getMessage().replace("\"", "\\\"");
String errorJson = "{\"error\":\"" + escaped + "\"}";
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorJson);
}
}
@GetMapping("/retrieve")
public ResponseEntity<List<FileResponse>> retrieveSpecificFile(@CookieValue(value = "id_token", required = false) String idTokenCookie) {
try {
// 1. 驗證並解析 token
FirebaseToken decoded = CookieAuthUtil.verifyIdToken(idTokenCookie);
String uid = decoded.getUid();

// 2. 查出所有該 user 的檔案
List<FileMetaData> files = fileStorageService.getFilesByUserId(decoded.getUid());

// 3. 用迴圈一張一張讀圖,放到新的 List 裡
List<FileResponse> images = new ArrayList<>();
for(FileMetaData file : files) {
// file.getUniqueId() 就是你 retrieveFile() 要傳的 fileId
FileResponse resp = fileStorageService.retrieveFile(file.getUniqueId());
images.add(resp);
}

return ResponseEntity.ok(images);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}



@GetMapping("/retrieve/{fileId}")
public ResponseEntity<FileResponse> retrieveFile(@PathVariable String fileId,
@CookieValue(value = "id_token", required = false) String idTokenCookie) {
try {
CookieAuthUtil.verifyIdToken(idTokenCookie);
FileResponse fileResponse = fileStorageService.retrieveFile(fileId);
return ResponseEntity.ok(fileResponse);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
}

CookieAuthUtil

/**
* 一個靜態工具類,用來驗證 id_token cookie。
*/
public class CookieAuthUtil {

/**
* 驗證傳入的 idToken(從 Cookie 拿到的值),
* 驗證成功回傳解碼後的 FirebaseToken,
* 失敗則丟 IllegalArgumentException。
*
* @param idTokenCookie 從 @CookieValue("id_token") 拿到的字串
* @return FirebaseToken 已驗證的 token
* @throws IllegalArgumentException 若 token 為空或驗證失敗
*/
public static FirebaseToken verifyIdToken(String idTokenCookie) {
if (!StringUtils.hasText(idTokenCookie)) {
throw new IllegalArgumentException("Missing ID token");
}
try {
return FirebaseAuth.getInstance().verifyIdToken(idTokenCookie);
} catch (FirebaseAuthException e) {
throw new IllegalArgumentException("Invalid ID token", e);
}
}

/**
* 簡單檢查 token 是否有效,回傳 boolean。
*
* @param idTokenCookie 從 Cookie 拿到的字串
* @return true = token 有效;false = token 為空或驗證失敗
*/
public static boolean isTokenValid(String idTokenCookie) {
try {
verifyIdToken(idTokenCookie);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}

FileStorageService

@Service
public class FileStorageService {

private final Storage storage;
private final FileMetaDataRepository repo;
private final String bucketName = "test-2ea39.firebasestorage.app";

public FileStorageService(FileMetaDataRepository repo) throws IOException {
this.repo = repo;

ClassPathResource resource = new ClassPathResource("testAccountKey.json");
InputStream serviceAccount = resource.getInputStream();

GoogleCredentials credentials = GoogleCredentials.fromStream(serviceAccount)
.createScoped(Lists.newArrayList("https://www.googleapis.com/auth/cloud-platform"));

this.storage = StorageOptions.newBuilder().setCredentials(credentials).build().getService();
}

/**
* 拿到某使用者所有上傳檔案的 metadata list
*/
public List<FileMetaData> getFilesByUserId(String userId) {
return repo.findAllByUserId(userId);
}

/**
* 拿單筆
*/
public FileMetaData getByUniqueId(String uniqueId) {
return repo.findByUniqueId(uniqueId);
}

public String uploadFile(MultipartFile file, String _uid) throws IOException {
if (file.isEmpty()) {
throw new IllegalArgumentException("File is empty. Please upload a valid file.");
}

String uniqueID = UUID.randomUUID().toString();
String objectName = uniqueID + "_" + file.getOriginalFilename();

BlobId blobId = BlobId.of(bucketName, objectName);

BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();

storage.create(blobInfo, file.getBytes());

FileMetaData metaData = new FileMetaData();
metaData.setUserId(_uid);
metaData.setUniqueId(uniqueID);
metaData.setObjectName(objectName);
metaData.setUploadDate(LocalDateTime.now());

repo.save(metaData);

return uniqueID;
}

public FileResponse retrieveFile(String fileId) {
FileMetaData fileMetadata = repo.findByUniqueId(fileId);

if (fileMetadata == null) {
throw new IllegalArgumentException("No file found with the given ID: " + fileId);
}

String objectName = fileMetadata.getObjectName();
BlobId blobId = BlobId.of(bucketName, objectName);
Blob blob = storage.get(blobId);

if (blob == null || !blob.exists()) {
throw new IllegalArgumentException("No file found with the given ID: " + fileId);
}

FileResponse fileResponse = new FileResponse(objectName, blob.getContent());
return fileResponse;
}
}

檔案上傳的範例 image

const handleupload = async () => {
if (!imageUrl) {
alert("請先上傳圖片");
return;
}
const input = document.getElementById("fileInput");
const file = input.files[0];

// 1. 建立 FormData 並 append 檔案
const formData = new FormData();
formData.append("file", file); // key 要跟 @RequestParam("file") 一致

const response = await fetch("http://localhost:8080/api/files/upload", {
method: "POST", // 請求方法:使用 POST,表示要向伺服器送出資料
mode: "cors", // CORS 模式——開啟跨域請求,允許向不同網域/埠號/協議的伺服器發出請求
credentials: "include", // 攜帶憑證——一定要加,當且僅當你需要和伺服器共享 Cookie、HTTP 認證資訊時使用
body: formData, // 請求主體:把前面用 new FormData() 建構好的表單資料(含檔案或欄位)當作請求內容送出
});

// 這裡可以加入 AI 修圖的邏輯
// 3. 讀結果
if (!response.ok) {
console.error("上傳失敗", await response.text());
} else {
console.log("上傳成功", await response.json());
}
};

點擊畫面的上傳 將圖片上傳後 點擊AI修圖(暫時設置為檔案上傳) 會在Console輸出上上傳成功,還有圖片儲存至firebase storage後,會產生objectName儲存進資料庫中,用作查詢圖片使用 image

在歷史圖片中,我是暫時將其設置為圖片儲存區 image 再開啟歷史圖片(圖片儲存區) 會先將cookie送至後端,後端會去firebase Authencation抓取user_id 再透過user_id去資料庫抓取objectName至firebase storage得取圖片顯示於畫面上

const fetchImages = async () => {
try {
const response = await fetch(
"http://localhost:8080/api/files/retrieve",
{
method: "GET", // 請求方法:使用 GET,表示要向伺服器請求資料
mode: "cors", // CORS 模式——開啟跨域請求,允許向不同網域/埠號/協議的伺服器發出請求
credentials: "include", // 攜帶憑證——一定要加,當且僅當你需要和伺服器共享 Cookie、HTTP 認證資訊時使用
}
);
// 後端傳回 [{ fileName, fileContent(Base64) }, ...]
const data = await response.json();

// 2️⃣ 把每筆 Base64 轉成可用的 data URL
const imgs = data.map((file) => {
// 根據副檔名判斷 MIME type
const ext = file.fileName.split(".").pop().toLowerCase();
const mime = ext === "png" ? "image/png" : "image/jpeg";
return {
name: file.fileName,
src: `data:${mime};base64,${file.fileContent}`,
};
});
if (!response.ok) {
console.error("上傳失敗");
} else {
console.log("上傳成功");
}
setImages(imgs);
} catch (error) {
console.error(error);
}
};