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
2 changes: 1 addition & 1 deletion pom-dependency-tree.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ai.elimu:webapp:war:2.5.108-SNAPSHOT
ai.elimu:webapp:war:2.5.109-SNAPSHOT
+- ai.elimu:model:jar:model-2.0.97:compile
| \- com.google.code.gson:gson:jar:2.13.0:compile
| \- com.google.errorprone:error_prone_annotations:jar:2.37.0:compile
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/ai/elimu/dao/VideoDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public interface VideoDao extends GenericDao<Video> {

Video read(String title) throws DataAccessException;

Video readByChecksumMd5(String checksumMd5) throws DataAccessException;

List<Video> readAllOrdered() throws DataAccessException;

List<Video> readAllOrderedById() throws DataAccessException;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/ai/elimu/dao/jpa/VideoDaoJpa.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ public Video read(String title) throws DataAccessException {
}
}

@Override
public Video readByChecksumMd5(String checksumMd5) throws DataAccessException {
try {
return (Video) em.createQuery(
"SELECT v " +
"FROM Video v " +
"WHERE v.checksumMd5 = :checksumMd5")
.setParameter("checksumMd5", checksumMd5)
.getSingleResult();
} catch (NoResultException e) {
return null;
}
}

@Override
public List<Video> readAllOrdered() throws DataAccessException {
return em.createQuery(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ public class Video extends Multimedia {
@NotNull
private Integer fileSize;

@NotNull
@Lob
@Column(length = 209715200) // 200MB
private byte[] bytes;

/**
* MD5 checksum of the file content.
*/
Expand All @@ -44,6 +39,7 @@ public class Video extends Multimedia {
@NotNull
private String checksumGitHub;

@Deprecated
@NotNull
@Lob
@Column(length = 1048576) // 1MB
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/ai/elimu/rest/v2/JpaToGsonConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ public static VideoGson getVideoGson(Video video) {
videoGson.setVideoFormat(video.getVideoFormat());
videoGson.setChecksumMd5(video.getChecksumMd5());
videoGson.setFileUrl(video.getUrl());
videoGson.setFileSize(video.getBytes().length / 1024);
videoGson.setFileSize(video.getFileSize());
videoGson.setThumbnailUrl("/video/" + video.getId() + "_r" + video.getRevisionNumber() + "_thumbnail.png");
Set<WordGson> wordGsons = new HashSet<>();
for (Word word : video.getWords()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ public String handleSubmit(
}
}

byte[] bytes = null;
try {
byte[] bytes = multipartFile.getBytes();
bytes = multipartFile.getBytes();
if (multipartFile.isEmpty() || (bytes == null) || (bytes.length == 0)) {
result.rejectValue("bytes", "NotNull");
} else {
Expand All @@ -89,9 +90,8 @@ public String handleSubmit(
log.info("contentType: " + contentType);
video.setContentType(contentType);

video.setBytes(bytes);
video.setFileSize(bytes.length);
video.setChecksumMd5(ChecksumHelper.calculateMD5(bytes));
// TODO: https://github.com/elimu-ai/webapp/issues/2137

// TODO: convert to a default video format?
}
Expand Down Expand Up @@ -122,9 +122,15 @@ public String handleSubmit(
return "content/multimedia/video/create";
} else {
video.setTitle(video.getTitle().toLowerCase());
String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, video.getBytes());
video.setChecksumGitHub(checksumGitHub);
video.setTimeLastUpdate(Calendar.getInstance());
Video existingVideoWithSameFileContent = videoDao.readByChecksumMd5(video.getChecksumMd5());
if (existingVideoWithSameFileContent != null) {
// Re-use existing file
video.setChecksumGitHub(existingVideoWithSameFileContent.getChecksumGitHub());
} else {
String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, bytes);
video.setChecksumGitHub(checksumGitHub);
}
videoDao.create(video);

// TODO: https://github.com/elimu-ai/webapp/issues/1545
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
@RequestMapping("/content/video/list/videos.csv")
@RequiredArgsConstructor
Expand All @@ -28,7 +27,7 @@ public void handleRequest(
log.info("handleRequest");

// Generate CSV file
String csvFileContent = "id,content_type,content_license,attribution_url,title,checksum_md5,file_url,video_format" + "\n";
String csvFileContent = "id,content_type,content_license,attribution_url,title,checksum_md5,file_url,file_size,video_format" + "\n";
List<Video> videos = videoDao.readAllOrderedById();
log.info("videos.size(): " + videos.size());
for (Video video : videos) {
Expand All @@ -39,6 +38,7 @@ public void handleRequest(
+ "\"" + video.getTitle() + "\","
+ "\"" + video.getChecksumMd5() + "\","
+ "\"" + video.getUrl() + "\","
+ "\"" + video.getFileSize() + "\","
+ video.getVideoFormat() + "\n";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,6 @@ public String handleRequest(
log.info("handleRequest");

Video video = videoDao.read(id);
if (StringUtils.isBlank(video.getChecksumGitHub())) {
String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, video.getBytes());
video.setChecksumGitHub(checksumGitHub);
video.setRevisionNumber(video.getRevisionNumber() + 1);
videoDao.update(video);

// TODO: https://github.com/elimu-ai/webapp/issues/1545
}
model.addAttribute("video", video);

model.addAttribute("contentLicenses", ContentLicense.values());
Expand Down Expand Up @@ -105,8 +97,9 @@ public String handleSubmit(
}
}

byte[] bytes = null;
try {
byte[] bytes = multipartFile.getBytes();
bytes = multipartFile.getBytes();
if (multipartFile.isEmpty() || (bytes == null) || (bytes.length == 0)) {
Comment on lines +100 to 103
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Guard against bytes == null before further processing

MultipartFile#getBytes() can throw IOException, in which case bytes remains null.
Because the catch-block only logs the exception, execution continues and bytes is later dereferenced (e.g. when calculating file size or uploading to GitHub LFS), which will trigger an NPE.

byte[] bytes = null;
try {
    bytes = multipartFile.getBytes();
+} catch (IOException e) {
+    log.error("Unable to read uploaded bytes", e);
+    result.rejectValue("bytes", "NotReadable");
+}
+
+if (bytes == null) {
+    // Bail out early – the rest of the logic depends on non-null bytes
+    model.addAttribute("video", video);
+    ...
+    return "content/multimedia/video/edit";
}

-if (multipartFile.isEmpty() || (bytes == null) || (bytes.length == 0)) {
+if (multipartFile.isEmpty() || (bytes.length == 0)) {
     result.rejectValue("bytes", "NotNull");

This ensures we never reach the later upload step with a null payload.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
byte[] bytes = null;
try {
byte[] bytes = multipartFile.getBytes();
bytes = multipartFile.getBytes();
if (multipartFile.isEmpty() || (bytes == null) || (bytes.length == 0)) {
byte[] bytes = null;
try {
bytes = multipartFile.getBytes();
} catch (IOException e) {
log.error("Unable to read uploaded bytes", e);
result.rejectValue("bytes", "NotReadable");
}
if (bytes == null) {
// Bail out early – the rest of the logic depends on non-null bytes
model.addAttribute("video", video);
// ... any other attributes you need to repopulate ...
return "content/multimedia/video/edit";
}
if (multipartFile.isEmpty() || (bytes.length == 0)) {
result.rejectValue("bytes", "NotNull");
}

result.rejectValue("bytes", "NotNull");
} else {
Expand All @@ -125,9 +118,8 @@ public String handleSubmit(
log.info("contentType: " + contentType);
video.setContentType(contentType);

video.setBytes(bytes);
video.setFileSize(bytes.length);
video.setChecksumMd5(ChecksumHelper.calculateMD5(bytes));
// TODO: https://github.com/elimu-ai/webapp/issues/2137

// TODO: convert to a default video format?
}
Expand All @@ -150,6 +142,14 @@ public String handleSubmit(
video.setTitle(video.getTitle().toLowerCase());
video.setTimeLastUpdate(Calendar.getInstance());
video.setRevisionNumber(video.getRevisionNumber() + 1);
Video existingVideoWithSameFileContent = videoDao.readByChecksumMd5(video.getChecksumMd5());
if (existingVideoWithSameFileContent != null) {
// Re-use existing file
video.setChecksumGitHub(existingVideoWithSameFileContent.getChecksumGitHub());
} else {
String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, bytes);
video.setChecksumGitHub(checksumGitHub);
}
Comment on lines +145 to +152
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

bytes may be null here, leading to NPE inside GitHubLfsHelper.uploadVideoToLfs

Even after the earlier null-protection, there is still a corner case: editing a video’s metadata without uploading a new file (multipartFile.isEmpty() == true).
In that flow bytes is never assigned and the deduplication branch falls through to uploadVideoToLfs, causing a crash.

Add an explicit guard:

if (existingVideoWithSameFileContent != null) {
    // Re-use existing file
    video.setChecksumGitHub(existingVideoWithSameFileContent.getChecksumGitHub());
-} else {
+} else if (bytes != null) {
    String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, bytes);
    video.setChecksumGitHub(checksumGitHub);
+} else {
+    // No new file supplied; keep the current GitHub checksum unchanged
}

This allows metadata-only edits while preserving the existing file reference.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Video existingVideoWithSameFileContent = videoDao.readByChecksumMd5(video.getChecksumMd5());
if (existingVideoWithSameFileContent != null) {
// Re-use existing file
video.setChecksumGitHub(existingVideoWithSameFileContent.getChecksumGitHub());
} else {
String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, bytes);
video.setChecksumGitHub(checksumGitHub);
}
Video existingVideoWithSameFileContent = videoDao.readByChecksumMd5(video.getChecksumMd5());
if (existingVideoWithSameFileContent != null) {
// Re-use existing file
video.setChecksumGitHub(existingVideoWithSameFileContent.getChecksumGitHub());
} else if (bytes != null) {
String checksumGitHub = GitHubLfsHelper.uploadVideoToLfs(video, bytes);
video.setChecksumGitHub(checksumGitHub);
} else {
// No new file supplied; keep the current GitHub checksum unchanged
}

videoDao.update(video);

// TODO: https://github.com/elimu-ai/webapp/issues/1545
Expand Down
42 changes: 0 additions & 42 deletions src/main/java/ai/elimu/web/download/VideoController.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,6 @@ public class VideoController {

private final VideoDao videoDao;

@GetMapping(value="/{videoId}_r{revisionNumber}.{videoFormat}")
public void handleRequest(
Model model,
@PathVariable Long videoId,
@PathVariable Integer revisionNumber,
@PathVariable String videoFormat,
HttpServletResponse response,
OutputStream outputStream) {
log.info("handleRequest");

log.info("videoId: " + videoId);
log.info("revisionNumber: " + revisionNumber);
log.info("videoFormat: " + videoFormat);

Video video = videoDao.read(videoId);

response.setContentType(video.getContentType());

byte[] bytes = video.getBytes();
response.setContentLength(bytes.length);
try {
outputStream.write(bytes);
} catch (EOFException ex) {
// org.eclipse.jetty.io.EofException (occurs when download is aborted before completion)
log.warn(ex.getMessage());
} catch (IOException ex) {
log.error(ex.getMessage());
} finally {
try {
try {
outputStream.flush();
outputStream.close();
} catch (EOFException ex) {
// org.eclipse.jetty.io.EofException (occurs when download is aborted before completion)
log.warn(ex.getMessage());
}
} catch (IOException ex) {
log.error(ex.getMessage());
}
}
}

@GetMapping(value = "/{videoId}_r{revisionNumber}_thumbnail.png")
public void handleThumbnailRequest(
Model model,
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/ai/elimu/web/servlet/CustomDispatcherServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ai.elimu.dao.SoundDao;
import ai.elimu.dao.StoryBookChapterDao;
import ai.elimu.dao.StoryBookDao;
import ai.elimu.dao.VideoDao;
import ai.elimu.dao.WordDao;
import ai.elimu.entity.content.Emoji;
import ai.elimu.entity.content.Letter;
Expand All @@ -19,10 +20,12 @@
import ai.elimu.entity.content.StoryBookChapter;
import ai.elimu.entity.content.Word;
import ai.elimu.entity.content.multimedia.Image;
import ai.elimu.entity.content.multimedia.Video;
import ai.elimu.entity.contributor.Contributor;
import ai.elimu.entity.enums.Role;
import ai.elimu.model.v2.enums.Environment;
import ai.elimu.model.v2.enums.content.ImageFormat;
import ai.elimu.model.v2.enums.content.VideoFormat;
import ai.elimu.util.ChecksumHelper;
import ai.elimu.util.ConfigHelper;
import ai.elimu.util.ImageColorHelper;
Expand Down Expand Up @@ -258,5 +261,25 @@ private void populateDatabase(WebApplicationContext webApplicationContext) {
storyBookChapter.setStoryBook(storyBook);
storyBookChapter.setSortOrder(0);
storyBookChapterDao.create(storyBookChapter);


VideoDao videoDao = (VideoDao) webApplicationContext.getBean("videoDao");

Video video = new Video();
video.setTitle("placeholder");
try {
ResourceLoader resourceLoader = new ClassRelativeResourceLoader(this.getClass());
Resource resource = resourceLoader.getResource("placeholder.mp4");
byte[] bytes = Files.readAllBytes(resource.getFile().toPath());
video.setFileSize(bytes.length);
video.setChecksumMd5(ChecksumHelper.calculateMD5(bytes));
video.setChecksumGitHub("1331d7c476649f4449ec7c6663fc107ce2b4d88b");
video.setThumbnail(Files.readAllBytes(new ClassRelativeResourceLoader(this.getClass()).getResource("placeholder.png").getFile().toPath()));
} catch (IOException e) {
logger.error(null, e);
}
video.setVideoFormat(VideoFormat.MP4);
video.setContentType("video/mp4");
videoDao.create(video);
}
}
1 change: 0 additions & 1 deletion src/main/resources/META-INF/jpa-schema-export.sql
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,6 @@
attributionUrl text,
contentLicense varchar(255),
contentType varchar(255),
bytes longblob,
checksumGitHub varchar(255),
checksumMd5 varchar(255),
fileSize integer,
Expand Down
Binary file not shown.
3 changes: 3 additions & 0 deletions src/main/resources/db/migration/2005109.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 2.5.109

ALTER TABLE `Video` DROP COLUMN `bytes`;
2 changes: 1 addition & 1 deletion src/main/webapp/WEB-INF/jsp/content/main.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
<span class="card-title"><i class="material-icons">movie</i> Videos</span>
</div>
<div class="card-action">
<a href="<spring:url value='/content/multimedia/video/list' />">View list (${videoCount})</a>
<a id="videoListLink" href="<spring:url value='/content/multimedia/video/list' />">View list (${videoCount})</a>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<div class="file-field input-field col s12">
<div class="btn">
<span>File (M4V/MP4)</span>
<form:input path="bytes" type="file" />
<input name="bytes" type="file" />
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
<div class="file-field input-field col s12">
<div class="btn">
<span>File (M4V/MP4)</span>
<form:input path="bytes" type="file" />
<input name="bytes" type="file" />
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text" />
Expand Down Expand Up @@ -358,6 +358,10 @@

<div class="divider" style="margin-bottom: 1em;"></div>

<label>file_size</label><br />
<code>${video.fileSize} bytes</code><br />
<br />

<label>checksum_md5</label><br />
<code>${video.checksumMd5}</code><br />
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@
</div>

<div class="fixed-action-btn" style="bottom: 2em; right: 2em;">
<a href="<spring:url value='/content/multimedia/video/create' />" class="btn-floating btn-large tooltipped" data-position="left" data-delay="50" data-tooltip="Add video"><i class="material-icons">add</i></a>
<a id="createButton" href="<spring:url value='/content/multimedia/video/create' />" class="btn-floating btn-large tooltipped" data-position="left" data-delay="50" data-tooltip="Add video"><i class="material-icons">add</i></a>
</div>
</content:section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ai.elimu.rest.v2.content;

import ai.elimu.util.JsonLoader;
import lombok.extern.slf4j.Slf4j;
import selenium.util.DomainHelper;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@Slf4j
public class VideosRestControllerTest {

@Test
public void testHandleGetRequest() {
String jsonResponse = JsonLoader.loadJson(DomainHelper.getRestUrlV2() + "/content/videos");
log.info("jsonResponse: " + jsonResponse);

JSONArray videosJSONArray = new JSONArray(jsonResponse);
log.info("videosJSONArray.length(): " + videosJSONArray.length());
assertFalse(videosJSONArray.isEmpty());

JSONObject videoJsonObject = videosJSONArray.getJSONObject(0);
assertNotNull(videoJsonObject.getString("title"));
}
}
5 changes: 5 additions & 0 deletions src/test/java/selenium/content/MainContentPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ public void pressStoryBookListLink() {
WebElement link = driver.findElement(By.id("storyBookListLink"));
link.click();
}

public void pressVideoListLink() {
WebElement link = driver.findElement(By.id("videoListLink"));
link.click();
}
}
15 changes: 15 additions & 0 deletions src/test/java/selenium/content/video/VideoCreatePage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package selenium.content.video;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class VideoCreatePage {

private WebDriver driver;

public VideoCreatePage(WebDriver driver) {
this.driver = driver;

driver.findElement(By.id("videoCreatePage"));
}
}
Loading
Loading