Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/src/main/java/com/ke/bella/files/api/FileController.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
import com.ke.bella.files.protocol.Scope;
import com.ke.bella.files.protocol.UpdateCitiesOps;
import com.ke.bella.files.protocol.UpdateDescriptionOps;
import com.ke.bella.files.protocol.UpdateMetadataOps;
import com.ke.bella.files.protocol.UpdateProgressRequestData;
import com.ke.bella.files.protocol.UpdateTagsOps;
import com.ke.bella.files.service.FileService;
Expand Down Expand Up @@ -1288,6 +1289,27 @@ public OpenAIFile updateDescription(
return fileService.updateFile(ops, false, Scope.DESCRIPTION);
}

@PutMapping("/{fileId}/metadata")
public OpenAIFile updateMetadata(
@PathVariable String fileId,
@RequestBody UpdateMetadataOps op) {
Assert.hasText(fileId, "file_id is required");
Assert.notNull(op, "invalid request body");
Assert.notNull(op.getMetadata(), "metadata is required");

OpenAIFile existingFile = fileService.getFile(fileId);
if(existingFile == null) {
throw new FileNotFoundException(fileId);
}
Comment on lines +1300 to +1303

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject metadata updates for resource nodes

When fileId identifies a resource node, this check accepts it and the endpoint overwrites its metadata. Resource creation deliberately fixes metadata to {}, and the adjacent description, cities, and tags update handlers reject resources; this also exceeds the stated ordinary-file-and-directory scope. Add the equivalent NodeType.RESOURCE guard before constructing the update operation.

Useful? React with 👍 / 👎.


FileOps ops = FileOps.builder()
.fileId(fileId)
.metadata(op.getMetadata())
.build();

return fileService.updateFile(ops, false, Scope.METADATA);
}

/**
* 更新城市信息
*
Expand Down
1 change: 1 addition & 0 deletions api/src/main/java/com/ke/bella/files/protocol/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum Scope {
DOM_TREE("dom_tree"),
PDF("pdf"),
DESCRIPTION("description"),
METADATA("metadata"),
CITIES("cities"),
TAGS("tags"),
LOCATION("location");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ke.bella.files.protocol;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UpdateMetadataOps {

private String metadata;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.ke.bella.files.api;

import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ke.bella.files.api.interceptor.FileApiResponseAdvice;
import com.ke.bella.files.protocol.FileOps;
import com.ke.bella.files.protocol.OpenAIFile;
import com.ke.bella.files.protocol.Scope;
import com.ke.bella.files.service.FileService;
import com.ke.bella.files.service.lock.FileUniquenessLock;

public class FileControllerUpdateMetadataTest {

private MockMvc mockMvc;
private FileService fileService;

@Before
public void setup() {
fileService = Mockito.mock(FileService.class);
FileController fileController = new FileController();
ReflectionTestUtils.setField(fileController, "fileService", fileService);
ReflectionTestUtils.setField(fileController, "fl", Mockito.mock(FileUniquenessLock.class));
mockMvc = MockMvcBuilders.standaloneSetup(fileController)
.setControllerAdvice(new FileApiResponseAdvice())
.setMessageConverters(new MappingJackson2HttpMessageConverter(new ObjectMapper()))
.build();
}

@Test
public void updateMetadata_FileSuccess() throws Exception {
assertSuccessfulUpdate("file-test-u", "file", false);
}

@Test
public void updateMetadata_DirectorySuccess() throws Exception {
assertSuccessfulUpdate("file-directory-u", "directory", true);
}

@Test
public void updateMetadata_MissingMetadata() throws Exception {
mockMvc.perform(put("/v1/files/{fileId}/metadata", "file-test-u")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error.message").value("metadata is required"));

verify(fileService, never()).getFile("file-test-u");
}

@Test
public void updateMetadata_FileNotFound() throws Exception {
when(fileService.getFile("file-missing-u")).thenReturn(null);

mockMvc.perform(put("/v1/files/{fileId}/metadata", "file-missing-u")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"metadata\":\"{\\\"team\\\":\\\"search\\\"}\"}"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error.message").exists());

verify(fileService).getFile("file-missing-u");
verify(fileService, never()).updateFile(Mockito.any(FileOps.class), eq(false), eq(Scope.METADATA));
}

private void assertSuccessfulUpdate(String fileId, String nodeType, boolean directory) throws Exception {
String oldMetadata = "{\"team\":\"old\"}";
String newMetadata = "{\"team\":\"search\"}";
OpenAIFile existingFile = OpenAIFile.builder()
.id(fileId)
.filename("test.txt")
.metadata(oldMetadata)
.nodeType(nodeType)
.isDir(directory)
.description("unchanged")
.version(7L)
.build();
OpenAIFile updatedFile = existingFile.toBuilder().metadata(newMetadata).build();
when(fileService.getFile(fileId)).thenReturn(existingFile);
when(fileService.updateFile(argThat(op -> fileId.equals(op.getFileId())
&& newMetadata.equals(op.getMetadata())
&& op.getFilename() == null
&& op.getDescription() == null
&& op.getCities() == null
&& op.getTags() == null), eq(false), eq(Scope.METADATA)))
.thenReturn(updatedFile);

mockMvc.perform(put("/v1/files/{fileId}/metadata", fileId)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"metadata\":\"{\\\"team\\\":\\\"search\\\"}\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(fileId))
.andExpect(jsonPath("$.metadata").value(newMetadata))
.andExpect(jsonPath("$.filename").value("test.txt"))
.andExpect(jsonPath("$.description").value("unchanged"))
.andExpect(jsonPath("$.version").value(7));

verify(fileService).getFile(fileId);
verify(fileService).updateFile(argThat(op -> fileId.equals(op.getFileId())
&& newMetadata.equals(op.getMetadata())), eq(false), eq(Scope.METADATA));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,21 @@ public void unchangedFilenameLeavesEntryUntouched() {
assertEquals(1, shardDsl.fetchCount(FILE_ENTRY, FILE_ENTRY.FILE_ID.eq(SOURCE)));
}

@Test
public void metadataUpdateLeavesOtherFileFieldsUntouched() {
FileDB before = queryFile(SOURCE);

fileRepo.updateFile(FileOps.builder().fileId(SOURCE).metadata("{\"team\":\"search\"}").build(), false);

FileDB after = queryFile(SOURCE);
assertEquals("{\"team\":\"search\"}", after.getMetaData());
assertEquals(before.getFilename(), after.getFilename());
assertEquals(before.getPurpose(), after.getPurpose());
assertEquals(before.getStatus(), after.getStatus());
assertEquals(before.getVersion(), after.getVersion());
assertEquals(queryEntryFilename(SOURCE), after.getFilename());
}

@Test
public void deleteHardDeletesEntry() {
fileRepo.updateFile(FileOps.builder().fileId(SOURCE).status(FileStatus.DELETED).build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ public void updateBroadcastsDomTreeSystemFile() {
assertBroadcastEvent(EventType.FILE_UPDATED);
}

@Test
public void metadataUpdateBroadcastsMetadataScope() {
FileDB file = file("file-test-u", FilePurpose.ASSISTANTS);
file.setMetaData("{\"team\":\"search\"}");
FileOps ops = FileOps.builder().fileId(file.getFileId()).metadata(file.getMetaData()).build();
when(fileRepo.queryFile(file.getFileId(), FileType.USER)).thenReturn(file);

fileService.updateFile(ops, false, Scope.METADATA);

ArgumentCaptor<FileBroadcasting> messageCaptor = ArgumentCaptor.forClass(FileBroadcasting.class);
verify(broadcastService).broadcast(messageCaptor.capture(), any(Runnable.class), any(Runnable.class));
assertEquals(EventType.FILE_UPDATED.getValue(), messageCaptor.getValue().getEvent());
assertEquals(Scope.METADATA.getValue(), messageCaptor.getValue().getScope());
assertEquals(file.getMetaData(), messageCaptor.getValue().getMetadata());
}

@Test
public void deleteSkipsBroadcastForSystemFile() {
FileDB file = systemFile(FilePurpose.DATASETS_EXPORT);
Expand Down
Loading