diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java index 13ffa1cdfba71f4970f2821bd4086dad5bcf16ad..59d1c688f5bef8e26ffdeb02dec85cb7d8265d21 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java +++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java @@ -26,6 +26,7 @@ import java.util.List; import org.olat.core.id.Identity; import org.olat.core.id.Roles; import org.olat.core.util.UserSession; +import org.olat.core.util.vfs.VFSContainer; import org.olat.group.BusinessGroup; import org.olat.modules.bigbluebutton.manager.BigBlueButtonUriBuilder; import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors; @@ -103,6 +104,18 @@ public interface BigBlueButtonManager { public BigBlueButtonMeeting getMeeting(BigBlueButtonMeeting meeting); + public VFSContainer getSlidesContainer(BigBlueButtonMeeting meeting); + + /** + * The method will create a meeting and uploaded the slides to the + * BigBlueButton server but it will only doing it during the leading + * time. + * + * @param meetingKey The meeting primary key + * @return true if slides were effectively uploaded + */ + public boolean preloadSlides(Long meetingKey); + /** * Return the first meeting which matches the specified identifier * as the meeting's identifier or readable identifier. diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java index 00acfd92d555586fd534d13c3a96a6b338d7aaec..07c55aaf23536ee4ca97d4c6b3d7f6d6c683667c 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java +++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonMeeting.java @@ -134,6 +134,14 @@ public interface BigBlueButtonMeeting extends ModifiedInfo, CreateInfo { public void setPassword(String password); + /** + * This is the relative path to the container for the slides. Generated + * only if needed. + * + * @return + */ + public String getDirectory(); + /** * If not a permanent meeting, the meetings starts at this date. Participants diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonModule.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonModule.java index 2f567905cee8b016fe00f5dffc6029d6cb70b7e9..8a3021f4d61b9802a3b6f16bb7623fa91d635878 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonModule.java +++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonModule.java @@ -20,6 +20,7 @@ package org.olat.modules.bigbluebutton; import java.net.URI; +import java.util.Set; import javax.ws.rs.core.UriBuilder; @@ -55,7 +56,13 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO private static final String PROP_ADHOC_MEETING = "vc.bigbluebutton.adhoc.meeting"; private static final String PROP_USER_BANDWIDTH_REQUIREMENT = "vc.bigbluebutton.user.bandwidth.requirement"; private static final String PROP_RECORDING_HANDLER_ID = "vc.bigbluebutton.recording.handler.id"; + private static final String PROP_MAX_UPLOAD_SIZE = "vc.bigbluebutton.max.upload.size"; + public static final Set<String> SLIDES_MIME_TYPES = Set.of("image/gif", "image/jpg", "image/jpeg", "image/png", "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + @Value("${vc.bigbluebutton.enabled}") private boolean enabled; @@ -78,6 +85,9 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO private String secret; @Value("${vc.bigbluebutton.shared.secret}") private String sharedSecret; + + @Value("${vc.bigbluebutton.max.upload.size:100}") + private Integer maxUploadSize; @Value("${vc.bigbluebutton.permanent.meeting:false}") private String permanentMeetingEnabled; @@ -123,6 +133,11 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO groupsEnabled = getStringPropertyValue(PROP_GROUP_ENABLED, groupsEnabled); coursesEnabled = getStringPropertyValue(PROP_COURSE_ENABLED, coursesEnabled); appointmentsEnabled = getStringPropertyValue(PROP_APPOINTMENTS_ENABLED, appointmentsEnabled); + + String maxUploadSizeObj = getStringPropertyValue(PROP_MAX_UPLOAD_SIZE, maxUploadSize.toString()); + if(StringHelper.containsNonWhitespace(maxUploadSizeObj)) { + maxUploadSize = Integer.valueOf(maxUploadSizeObj); + } String bandwidthReqObj = getStringPropertyValue(PROP_USER_BANDWIDTH_REQUIREMENT, true); if(StringHelper.containsNonWhitespace(bandwidthReqObj)) { @@ -283,6 +298,20 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO setStringProperty(PROP_SHARED_SECRET, sharedSecret, true); } + /** + * + * @return The max. size of the slides to upload to BigBlueButton servers + * in MB. + */ + public Integer getMaxUploadSize() { + return maxUploadSize; + } + + public void setMaxUploadSize(Integer maxUploadSize) { + this.maxUploadSize = maxUploadSize; + setStringProperty(PROP_MAX_UPLOAD_SIZE, maxUploadSize.toString(), true); + } + public Double getUserBandwidhtRequirement() { return userBandwidhtRequirement; } diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java index d2144bc3dad7d23e62c3d6b22269e53a5144f039..f905f476b1704b75ca0feb0c291eb689ba0b1b48 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java +++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java @@ -20,6 +20,7 @@ package org.olat.modules.bigbluebutton.manager; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; @@ -34,9 +35,13 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.Logger; @@ -47,7 +52,13 @@ import org.olat.commons.calendar.model.KalendarEvent; import org.olat.commons.calendar.model.KalendarEventLink; import org.olat.commons.calendar.ui.components.KalendarRenderWrapper; import org.olat.core.commons.persistence.DB; +import org.olat.core.commons.services.taskexecutor.Task; +import org.olat.core.commons.services.taskexecutor.TaskExecutorManager; +import org.olat.core.dispatcher.mapper.MapperService; +import org.olat.core.dispatcher.mapper.manager.MapperKey; +import org.olat.core.helpers.Settings; import org.olat.core.id.Identity; +import org.olat.core.id.OLATResourceable; import org.olat.core.id.Roles; import org.olat.core.id.User; import org.olat.core.id.context.BusinessControlFactory; @@ -56,6 +67,11 @@ import org.olat.core.util.CodeHelper; import org.olat.core.util.StringHelper; import org.olat.core.util.UserSession; import org.olat.core.util.WebappHelper; +import org.olat.core.util.resource.OresHelper; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter; import org.olat.course.CourseFactory; import org.olat.course.ICourse; import org.olat.group.BusinessGroup; @@ -90,6 +106,8 @@ import org.olat.repository.RepositoryEntryRef; import org.olat.repository.RepositoryEntrySecurity; import org.olat.repository.RepositoryManager; import org.olat.repository.manager.RepositoryEntryDAO; +import org.olat.resource.OLATResource; +import org.olat.resource.OLATResourceManager; import org.olat.user.UserDataDeletable; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; @@ -108,12 +126,19 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, DeletableGroupData, RepositoryEntryDataDeletable, UserDataDeletable, InitializingBean { private static final Logger log = Tracing.createLoggerFor(BigBlueButtonManagerImpl.class); + private static final String TASK_MEETING_RESNAME = BigBlueButtonMeeting.class.getSimpleName(); @Autowired private DB dbInstance; @Autowired + private MapperService mapperService; + @Autowired private CalendarManager calendarManager; @Autowired + private TaskExecutorManager taskManager; + @Autowired + private OLATResourceManager resourceManager; + @Autowired private RepositoryManager repositoryManager; @Autowired private RepositoryEntryDAO repositoryEntryDao; @@ -128,6 +153,8 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, @Autowired private BigBlueButtonAttendeeDAO bigBlueButtonAttendeeDao; @Autowired + private BigBlueButtonSlidesStorage bigBlueButtonSlidesStorage; + @Autowired private BigBlueButtonMeetingTemplateDAO bigBlueButtonMeetingTemplateDao; @Autowired private BigBlueButtonRecordingReferenceDAO bigBlueButtonRecordingReferenceDao; @@ -386,6 +413,41 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, return null; } + @Override + public VFSContainer getSlidesContainer(BigBlueButtonMeeting meeting) { + VFSContainer container; + if(StringHelper.containsNonWhitespace(meeting.getDirectory())) { + container = bigBlueButtonSlidesStorage.getStorage(meeting); + } else { + container = bigBlueButtonSlidesStorage.createStorage(meeting); + } + return container; + } + + @Override + public boolean preloadSlides(Long meetingKey) { + BigBlueButtonMeeting meeting = bigBlueButtonMeetingDao.loadByKey(meetingKey); + if(meeting == null) { + return false; + } + + boolean loaded = false; + Date now = new Date(); + Date start = meeting.getStartDate(); + Date startWithLeadingTime = meeting.getStartWithLeadTime(); + if(startWithLeadingTime.compareTo(now) <= 0 && start.compareTo(now) >= 0) { + List<VFSLeaf> slides = getSlides(meeting); + if(!slides.isEmpty()) { + BigBlueButtonErrors errors = new BigBlueButtonErrors(); + meeting = getMeetingWithServer(meeting); + createBigBlueButtonMeeting(meeting, errors); + loaded = !errors.hasErrors(); + log.info(Tracing.M_AUDIT, "Slides preloaded: {}", meeting); + } + } + return loaded; + } + @Override public boolean isIdentifierInUse(String identifier, BigBlueButtonMeeting reference) { if(StringHelper.containsNonWhitespace(identifier)) { @@ -397,7 +459,23 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, @Override public BigBlueButtonMeeting updateMeeting(BigBlueButtonMeeting meeting) { updateCalendarEvent(meeting); - return bigBlueButtonMeetingDao.updateMeeting(meeting); + meeting = bigBlueButtonMeetingDao.updateMeeting(meeting); + if(StringHelper.containsNonWhitespace(meeting.getDirectory())) { + OLATResource resource = resourceManager.findResourceable(meeting.getKey(), TASK_MEETING_RESNAME); + if(resource == null) { + OLATResourceable res = OresHelper.createOLATResourceableInstance(TASK_MEETING_RESNAME, meeting.getKey()); + resource = resourceManager.createAndPersistOLATResourceInstance(res); + SlidesPreloaderTask loader = new SlidesPreloaderTask(meeting.getKey()); + taskManager.execute(loader, null, resource, null, meeting.getStartWithLeadTime()); + } else { + List<Task> currentTasks = taskManager.getTasks(resource); + if(!currentTasks.isEmpty()) { + SlidesPreloaderTask loader = new SlidesPreloaderTask(meeting.getKey()); + taskManager.updateAndReturn(currentTasks.get(0), loader, null, meeting.getStartWithLeadTime()); + } + } + } + return meeting; } @Override @@ -460,12 +538,28 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, bigBlueButtonMeetingDeletionHandlers.forEach(h -> h.onBeforeDelete(reloadedMeeting)); removeCalendarEvent(reloadedMeeting); deleteRecordings(meeting, errors); + deleteSlides(meeting); bigBlueButtonAttendeeDao.deleteAttendee(reloadedMeeting); bigBlueButtonMeetingDao.deleteMeeting(reloadedMeeting); } return false; } + private void deleteSlides(BigBlueButtonMeeting meeting) { + if(StringHelper.containsNonWhitespace(meeting.getDirectory())) { + VFSContainer slidesContainer = bigBlueButtonSlidesStorage.getStorage(meeting); + if(slidesContainer != null && slidesContainer.exists()) { + slidesContainer.deleteSilently(); + } + + OLATResource resource = resourceManager.findResourceable(meeting.getKey(), TASK_MEETING_RESNAME); + if(resource != null) { + taskManager.delete(resource); + resourceManager.deleteOLATResource(resource); + } + } + } + public BigBlueButtonServer getAvailableServer() { List<BigBlueButtonServer> servers = getServers(); List<BigBlueButtonServer> availableServers = servers.stream() @@ -948,10 +1042,52 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, // metadata getRecordingsHandler().appendMetadata(uriBuilder, meeting); + // slides + if(StringHelper.containsNonWhitespace(meeting.getDirectory())) { + VFSContainer slidesContainer = bigBlueButtonSlidesStorage.getStorage(meeting); + List<VFSLeaf> slides = getSlides(meeting); + if(!slides.isEmpty()) { + MapperKey mapperKey = mapperService.register(null, meeting.getMeetingId(), new SlidesContainerMapper(slidesContainer), 360); + String url = Settings.createServerURI() + mapperKey.getUrl() + "/slides/"; + String slidesXml = slidesDocument(url, slides); + uriBuilder.xmlPayload(slidesXml); + } + } + Document doc = sendRequest(uriBuilder, errors); return BigBlueButtonUtils.checkSuccess(doc, errors); } + private String slidesDocument(String url, List<VFSLeaf> slides) { + StringBuilder sb = new StringBuilder(); + sb.append("<?xml version='1.0' encoding='UTF-8'?>") + .append("<modules><module name='presentation'>"); + + for(VFSLeaf slide:slides) { + sb.append("<document url='").append(url).append(slide.getName()).append("' />"); + } + sb.append("</module></modules>"); + return sb.toString(); + } + + private List<VFSLeaf> getSlides(BigBlueButtonMeeting meeting) { + List<VFSLeaf> slides = new ArrayList<>(); + + if(StringHelper.containsNonWhitespace(meeting.getDirectory())) { + VFSContainer slidesContainer = bigBlueButtonSlidesStorage.getStorage(meeting); + if(slidesContainer != null && slidesContainer.exists()) { + List<VFSItem> items = slidesContainer.getItems(new VFSLeafButSystemFilter()); + for(VFSItem item:items) { + if(item instanceof VFSLeaf) { + slides.add((VFSLeaf)item); + } + } + } + } + + return slides; + } + @Override public BigBlueButtonAttendee getAttendee(Identity identity, BigBlueButtonMeeting meeting) { return bigBlueButtonAttendeeDao.getAttendee(identity, meeting); @@ -1032,14 +1168,41 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, @Override public Document sendRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) { dbInstance.commit(); + if(StringHelper.containsNonWhitespace(builder.getXmlPayload())) { + return sendPostRequest(builder, errors); + } + return sendGetRequest(builder, errors); + } + + private Document sendPostRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) { + URI uri = builder.build(); + HttpPost post = new HttpPost(uri); + String payload = builder.getXmlPayload(); + post.addHeader("Content-Language", "en-US"); + ContentType cType = ContentType.create("text/xml", StandardCharsets.UTF_8); + HttpEntity myEntity = new StringEntity(payload, cType); + post.setEntity(myEntity); + RequestConfig requestConfig = getRequestConfiguration(); + try(CloseableHttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .disableAutomaticRetries() + .build(); + CloseableHttpResponse response = httpClient.execute(post)) { + int statusCode = response.getStatusLine().getStatusCode(); + log.debug("Status code of: {} {}", uri, statusCode); + return BigBlueButtonUtils.getDocumentFromEntity(response.getEntity()); + } catch(Exception e) { + errors.append(new BigBlueButtonError(BigBlueButtonErrorCodes.unkown)); + log.error("Cannot send: {}", uri, e); + return null; + } + } + + private Document sendGetRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) { URI uri = builder.build(); HttpGet get = new HttpGet(uri); - RequestConfig requestConfig = RequestConfig.copy(RequestConfig.DEFAULT) - .setConnectTimeout(bigBlueButtonModule.getHttpConnectTimeout()) - .setConnectionRequestTimeout(bigBlueButtonModule.getHttpConnectRequestTimeout()) - .setSocketTimeout(bigBlueButtonModule.getHttpSocketTimeout()) - .build(); + RequestConfig requestConfig = getRequestConfiguration(); try(CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultRequestConfig(requestConfig) .disableAutomaticRetries() @@ -1055,6 +1218,14 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager, } } + private RequestConfig getRequestConfiguration() { + return RequestConfig.copy(RequestConfig.DEFAULT) + .setConnectTimeout(bigBlueButtonModule.getHttpConnectTimeout()) + .setConnectionRequestTimeout(bigBlueButtonModule.getHttpConnectRequestTimeout()) + .setSocketTimeout(bigBlueButtonModule.getHttpSocketTimeout()) + .build(); + } + private static class ServerLoadComparator implements Comparator<BigBlueButtonServerInfos> { @Override diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java index e90f713ac04997dfbeea17faba4f805b7da245a4..5d1dc7bfab2b94694f97fa8a9076bdc99060159c 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java +++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonMeetingDAO.java @@ -79,6 +79,7 @@ public class BigBlueButtonMeetingDAO { meeting.setCreator(creator); dbInstance.getCurrentEntityManager().persist(meeting); + return meeting; } diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonSlidesStorage.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonSlidesStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..2f975b1d7db75d137e196191cc4a8ca4a6edda71 --- /dev/null +++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonSlidesStorage.java @@ -0,0 +1,78 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.bigbluebutton.manager; + +import java.io.File; + +import org.olat.core.util.StringHelper; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSManager; +import org.olat.modules.bigbluebutton.BigBlueButtonMeeting; +import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl; +import org.springframework.stereotype.Service; + +/** + * + * Initial date: 22 déc. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +@Service +public class BigBlueButtonSlidesStorage { + + private static final String DIRECTORY = "bigbluebutton"; + + private VFSContainer rootContainer; + + public VFSContainer getRootContainer() { + if(rootContainer == null) { + rootContainer = VFSManager.olatRootContainer(File.separator + DIRECTORY, null); + } + return rootContainer; + } + + public VFSContainer getStorage(BigBlueButtonMeeting meeting) { + VFSItem dir = getRootContainer().resolve(meeting.getDirectory()); + return dir instanceof VFSContainer ? (VFSContainer)dir : null; + } + + public VFSContainer createStorage(BigBlueButtonMeeting meeting) { + StringBuilder sb = new StringBuilder(); + if(meeting.getBusinessGroup() != null) { + VFSManager.getOrCreateContainer(getRootContainer(), sb.toString()); + sb.append("group").append(File.separator).append(meeting.getBusinessGroup().getKey()); + } else if(meeting.getEntry() != null) { + sb.append("course").append(File.separator).append(meeting.getEntry().getKey()).append(File.separator); + if(StringHelper.containsNonWhitespace(meeting.getSubIdent())) { + sb.append(meeting.getSubIdent()); + } else { + sb.append("tool"); + } + } + + sb.append(File.separator).append(meeting.getKey()); + + VFSContainer cont = VFSManager.olatRootContainer(File.separator + DIRECTORY + File.separator + sb.toString()); + + ((BigBlueButtonMeetingImpl)meeting).setDirectory(sb.toString()); + return cont; + } +} diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUriBuilder.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUriBuilder.java index dd2d71132ce1b2b21b5f01ea743f90fb4333f7bc..dff97948f5817b0ea8355b7e9b8216e5ed09feb8 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUriBuilder.java +++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonUriBuilder.java @@ -42,6 +42,7 @@ public class BigBlueButtonUriBuilder { private String scheme; private int port; private String startPath; + private String xmlPayload; private final String sharedSecret; @@ -64,6 +65,11 @@ public class BigBlueButtonUriBuilder { return new BigBlueButtonUriBuilder(uri, sharedSecret); } + public BigBlueButtonUriBuilder xmlPayload(String xml) { + xmlPayload = xml; + return this; + } + public BigBlueButtonUriBuilder operation(String operation) { this.operation = operation; return this; @@ -133,6 +139,10 @@ public class BigBlueButtonUriBuilder { } } + public String getXmlPayload() { + return xmlPayload; + } + public static String urlEncode(String s) { try { return URLEncoder.encode(s, "UTF-8"); diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/SlidesContainerMapper.java b/src/main/java/org/olat/modules/bigbluebutton/manager/SlidesContainerMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..cacf113b41d16abe9cce5bf9881c7311b88527b0 --- /dev/null +++ b/src/main/java/org/olat/modules/bigbluebutton/manager/SlidesContainerMapper.java @@ -0,0 +1,77 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.bigbluebutton.manager; + +import javax.servlet.http.HttpServletRequest; + +import org.olat.core.dispatcher.mapper.Mapper; +import org.olat.core.gui.media.ForbiddenMediaResource; +import org.olat.core.gui.media.MediaResource; +import org.olat.core.gui.media.NotFoundMediaResource; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.VFSMediaResource; + +/** + * + * Initial date: 23 déc. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SlidesContainerMapper implements Mapper { + + public static final String DOWNLOAD_PREFIX = "/slides/"; + + private final VFSContainer container; + private final VFSContainer tempContainer; + + public SlidesContainerMapper(VFSContainer container) { + this(null, container); + } + + public SlidesContainerMapper(VFSContainer tempContainer, VFSContainer container) { + this.container = container; + this.tempContainer = tempContainer; + } + + @Override + public MediaResource handle(String relPath, HttpServletRequest request) { + MediaResource resource = null; + if(relPath.startsWith(DOWNLOAD_PREFIX)) { + String filename = relPath.substring(DOWNLOAD_PREFIX.length()); + VFSItem slide = null; + if(tempContainer != null) { + slide = tempContainer.resolve(filename); + } + if(slide == null && container != null) { + slide = container.resolve(filename); + } + if(slide instanceof VFSLeaf) { + resource = new VFSMediaResource((VFSLeaf)slide); + } else { + resource = new NotFoundMediaResource(); + } + } else { + resource = new ForbiddenMediaResource(); + } + return resource; + } +} diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/SlidesPreloaderTask.java b/src/main/java/org/olat/modules/bigbluebutton/manager/SlidesPreloaderTask.java new file mode 100644 index 0000000000000000000000000000000000000000..466ed7eb189b77be811242dc55294a633b90fe14 --- /dev/null +++ b/src/main/java/org/olat/modules/bigbluebutton/manager/SlidesPreloaderTask.java @@ -0,0 +1,47 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.bigbluebutton.manager; + +import org.olat.core.CoreSpringFactory; +import org.olat.core.commons.services.taskexecutor.LongRunnable; +import org.olat.modules.bigbluebutton.BigBlueButtonManager; + +/** + * + * Initial date: 23 déc. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SlidesPreloaderTask implements LongRunnable { + + private static final long serialVersionUID = -1753449764226330508L; + + private Long meetingKey; + + public SlidesPreloaderTask(Long meetingKey) { + this.meetingKey = meetingKey; + } + + @Override + public void run() { + CoreSpringFactory.getImpl(BigBlueButtonManager.class) + .preloadSlides(meetingKey); + } +} diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java index e5176ddefc7b4cc1413fae3ffe3b72db4149780e..4cc0c3771f8f9f49647f6ccd5cd7a55cd83a17f4 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java +++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonMeetingImpl.java @@ -107,6 +107,9 @@ public class BigBlueButtonMeetingImpl implements Persistable, BigBlueButtonMeeti @Column(name="b_layout", nullable=false, insertable=true, updatable=true) private String layout; + @Column(name="b_directory", nullable=true, insertable=true, updatable=true) + private String directory; + @Column(name="b_meeting_id", nullable=false, insertable=true, updatable=false) private String meetingId; @Column(name="b_attendee_pw", nullable=false, insertable=true, updatable=false) @@ -228,8 +231,14 @@ public class BigBlueButtonMeetingImpl implements Persistable, BigBlueButtonMeeti public void setWelcome(String welcome) { this.welcome = welcome; } - - + + public String getDirectory() { + return directory; + } + + public void setDirectory(String directory) { + this.directory = directory; + } public String getLayout() { return layout; diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java index bb6e97ca7cae860f1a7e9a072f2eb35748b4ad60..d39d4c26a21faf0a5cc56265ffd113da725b8259 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonConfigurationController.java @@ -29,6 +29,7 @@ import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement; import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement; import org.olat.core.gui.components.form.flexible.elements.SingleSelection; +import org.olat.core.gui.components.form.flexible.elements.TextElement; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; @@ -42,6 +43,7 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController; +import org.olat.core.util.StringHelper; import org.olat.modules.bigbluebutton.BigBlueButtonManager; import org.olat.modules.bigbluebutton.BigBlueButtonModule; import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsHandler; @@ -65,6 +67,7 @@ public class BigBlueButtonConfigurationController extends FormBasicController { private MultipleSelectionElement enabledForEl; private MultipleSelectionElement permanentForEl; private SingleSelection recordingsHandlerEl; + private TextElement slidesUploadLimitEl; private FormLink addServerButton; private FlexiTableElement serversTableEl; @@ -140,6 +143,11 @@ public class BigBlueButtonConfigurationController extends FormBasicController { } else { recordingsHandlerEl.select(BigBlueButtonNativeRecordingsHandler.NATIVE_RECORDING_HANDLER_ID, true); } + + Integer maxSize = bigBlueButtonModule.getMaxUploadSize(); + String maxSizeStr = maxSize == null ? null : maxSize.toString(); + slidesUploadLimitEl = uifactory.addTextElement("slides.upload.limit", 8, maxSizeStr, formLayout); + slidesUploadLimitEl.setMandatory(true); //buttons save - check FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("save", getTranslator()); @@ -185,6 +193,19 @@ public class BigBlueButtonConfigurationController extends FormBasicController { } } + slidesUploadLimitEl.clearError(); + if(StringHelper.containsNonWhitespace(slidesUploadLimitEl.getValue())) { + try { + Integer.parseInt(slidesUploadLimitEl.getValue()); + } catch (NumberFormatException e) { + slidesUploadLimitEl.setErrorKey("form.error.nointeger", null); + allOk &= false; + } + } else { + slidesUploadLimitEl.setErrorKey("form.legende.mandatory", null); + allOk &= false; + } + return allOk; } @@ -241,6 +262,7 @@ public class BigBlueButtonConfigurationController extends FormBasicController { bigBlueButtonModule.setGroupsEnabled(enabledForEl.isSelected(2)); bigBlueButtonModule.setPermanentMeetingEnabled(permanentForEl.isAtLeastSelected(1)); bigBlueButtonModule.setRecordingHandlerId(recordingsHandlerEl.getSelectedKey()); + bigBlueButtonModule.setMaxUploadSize(Integer.valueOf(slidesUploadLimitEl.getValue())); } CollaborationToolsFactory.getInstance().initAvailableTools(); } diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java index 00e682482f1a44cd8537429b5c8bf1a845c274b2..2b81cb3ccd6c3722b1b6298ed7436ea1ca928be0 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java @@ -46,6 +46,7 @@ import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; import org.olat.core.gui.control.WindowControl; import org.olat.core.gui.control.generic.closablewrapper.CloseableCalloutWindowController; +import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController; import org.olat.core.gui.control.generic.modal.DialogBoxController; import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory; import org.olat.core.gui.control.winmgr.CommandFactory; @@ -57,6 +58,10 @@ import org.olat.core.util.UserSession; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.event.GenericEventListener; import org.olat.core.util.resource.OresHelper; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter; import org.olat.modules.bigbluebutton.BigBlueButtonAttendee; import org.olat.modules.bigbluebutton.BigBlueButtonAttendeeRoles; import org.olat.modules.bigbluebutton.BigBlueButtonDispatcher; @@ -66,6 +71,7 @@ import org.olat.modules.bigbluebutton.BigBlueButtonModule; import org.olat.modules.bigbluebutton.BigBlueButtonRecording; import org.olat.modules.bigbluebutton.BigBlueButtonRecordingReference; import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsPublishedRoles; +import org.olat.modules.bigbluebutton.manager.SlidesContainerMapper; import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors; import org.olat.modules.bigbluebutton.model.BigBlueButtonRecordingWithReference; import org.olat.modules.bigbluebutton.ui.BigBlueButtonRecordingTableModel.BRecordingsCols; @@ -84,16 +90,22 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen private final boolean administrator; private BigBlueButtonMeeting meeting; + private int count = 0; private final boolean guest; private final boolean moderatorStartMeeting; private final OLATResourceable meetingOres; private FormLink joinButton; + private FormLink uploadButton; private FormLink guestJoinButton; private MultipleSelectionElement acknowledgeRecordingEl; private FlexiTableElement tableEl; private BigBlueButtonRecordingTableModel recordingTableModel; + + private SlidesContainerMapper slidesMapper; + private CloseableModalController cmc; + private SlideUploadController uploadSlideCtrl; private PublishRecordingController publishCtrl; private DialogBoxController confirmDeleteRecordingDialog; private CloseableCalloutWindowController publishCalloutCtrl; @@ -162,6 +174,11 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen layoutCont.contextPut("externalPassword", password); } + if(administrator || moderator) { + loadSlides(layoutCont); + uploadButton = uifactory.addFormLink("meeting.slides.upload", formLayout, Link.BUTTON_SMALL); + } + if(StringHelper.containsNonWhitespace(meeting.getMainPresenter())) { layoutCont.contextPut("mainPresenter", meeting.getMainPresenter()); } @@ -185,6 +202,38 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen acknowledgeKeyValue.keys(), acknowledgeKeyValue.values()); } + private void loadSlides(FormLayoutContainer layoutCont) { + List<SlideWrapper> documentWrappers = new ArrayList<>(); + if(StringHelper.containsNonWhitespace(meeting.getDirectory())) { + VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting); + if(slidesMapper == null) { + slidesMapper = new SlidesContainerMapper(slidesContainer); + String mapperUri = registerCacheableMapper(null, "BigBlueButtonSlides::" + meeting.getKey(), slidesMapper); + layoutCont.contextPut("mapperUri", mapperUri); + } + + if(slidesContainer != null && slidesContainer.exists()) { + boolean slidesEditable = isSlidesEditable(); + List<VFSItem> items = slidesContainer.getItems(new VFSLeafButSystemFilter()); + for(VFSItem item:items) { + if(item instanceof VFSLeaf) { + VFSLeaf slide = (VFSLeaf)item; + SlideWrapper wrapper = new SlideWrapper(slide, false); + if(slidesEditable) { + FormLink deleteButton = uifactory + .addFormLink("delete_" + (++count), "delete", "delete", null, layoutCont, Link.BUTTON_XSMALL); + deleteButton.setUserObject(wrapper); + wrapper.setDeleteButton(deleteButton); + } + documentWrappers.add(wrapper); + } + } + } + } + + layoutCont.contextPut("documents", documentWrappers); + } + private void initRecordings(FormItemContainer formLayout) { FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel(); columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(BRecordingsCols.name)); @@ -273,6 +322,17 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen return !((start != null && start.compareTo(now) >= 0) || (end != null && end.compareTo(now) <= 0)); } + private boolean isSlidesEditable() { + if(meeting == null) return false; + if(meeting.isPermanent()) { + return true; + } + + Date now = new Date(); + Date start = meeting.getStartDate(); + return start != null && start.compareTo(now) > 0; + } + private void reloadButtonsAndStatus() { meeting = bigBlueButtonManager.getMeeting(meeting); updateButtonsAndStatus(); @@ -300,6 +360,11 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen guestJoinButton.setVisible(accessible && !disabled && guest); } guestJoinButton.setEnabled(!readOnly && accessible && !disabled && guest); + + if(uploadButton != null) { + boolean slidesEditable = isSlidesEditable(); + uploadButton.setVisible(slidesEditable); + } if(accessible && !disabled) { boolean running = bigBlueButtonManager.isMeetingRunning(meeting); @@ -364,7 +429,14 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen } publishCalloutCtrl.deactivate(); cleanUp(); - } else if(publishCalloutCtrl == source) { + } else if(uploadSlideCtrl == source) { + if(event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) { + meeting = bigBlueButtonManager.getMeeting(meeting); + loadSlides(flc); + } + cmc.deactivate(); + cleanUp(); + } else if(publishCalloutCtrl == source || cmc == source) { cleanUp(); } super.event(ureq, source, event); @@ -373,10 +445,14 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen private void cleanUp() { removeAsListenerAndDispose(confirmDeleteRecordingDialog); removeAsListenerAndDispose(publishCalloutCtrl); + removeAsListenerAndDispose(uploadSlideCtrl); removeAsListenerAndDispose(publishCtrl); + removeAsListenerAndDispose(cmc); confirmDeleteRecordingDialog = null; publishCalloutCtrl = null; + uploadSlideCtrl = null; publishCtrl = null; + cmc = null; } @Override @@ -401,6 +477,8 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen doJoin(ureq); } else if(this.guestJoinButton == source) { doGuestJoin(ureq); + } else if(uploadButton == source) { + doUploadSlides(ureq); } else if(tableEl == source) { if(event instanceof SelectionEvent) { SelectionEvent se = (SelectionEvent)event; @@ -414,6 +492,9 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen FormLink link = (FormLink)source; if("publish".equals(link.getCmd()) && link.getUserObject() instanceof BigBlueButtonRecordingRow) { doPublish(ureq, link, (BigBlueButtonRecordingRow)link.getUserObject()); + } else if("delete".equals(link.getCmd()) && link.getUserObject() instanceof SlideWrapper) { + doDeleteSlide((SlideWrapper)link.getUserObject()); + loadSlides(flc); } } super.formInnerEvent(ureq, source, event); @@ -424,6 +505,25 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen // } + private void doUploadSlides(UserRequest ureq) { + uploadSlideCtrl = new SlideUploadController(ureq, getWindowControl(), meeting); + listenTo(uploadSlideCtrl); + + String title = translate("meeting.slides.upload"); + cmc = new CloseableModalController(getWindowControl(), translate("close"), uploadSlideCtrl.getInitialComponent(), true, title); + listenTo(cmc); + cmc.activate(); + } + + private void doDeleteSlide(SlideWrapper slide) { + VFSLeaf document = slide.getDocument(); + VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting); + VFSItem reloadedDocument = slidesContainer.resolve(document.getName()); + if(reloadedDocument != null && reloadedDocument.exists()) { + reloadedDocument.delete(); + } + } + private void doPublish(UserRequest ureq, FormLink link, BigBlueButtonRecordingRow row) { publishCtrl = new PublishRecordingController(ureq, getWindowControl(), row); listenTo(publishCtrl); diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonMeetingController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonMeetingController.java index 15ce960d1d57ae749f6e316cf1612939057b8fb6..8ded0af2c06952b9af081aebd49b4c4c95cf2211 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonMeetingController.java +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/EditBigBlueButtonMeetingController.java @@ -22,14 +22,19 @@ package org.olat.modules.bigbluebutton.ui; import static org.olat.modules.bigbluebutton.ui.BigBlueButtonUIHelper.getSelectedTemplate; import static org.olat.modules.bigbluebutton.ui.BigBlueButtonUIHelper.isWebcamLayoutAvailable; +import java.io.File; +import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.form.flexible.FormItem; import org.olat.core.gui.components.form.flexible.FormItemContainer; import org.olat.core.gui.components.form.flexible.elements.DateChooser; +import org.olat.core.gui.components.form.flexible.elements.FileElement; import org.olat.core.gui.components.form.flexible.elements.FormLink; import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement; import org.olat.core.gui.components.form.flexible.elements.SingleSelection; @@ -37,6 +42,7 @@ import org.olat.core.gui.components.form.flexible.elements.TextElement; import org.olat.core.gui.components.form.flexible.impl.FormBasicController; import org.olat.core.gui.components.form.flexible.impl.FormEvent; import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.components.link.Link; import org.olat.core.gui.components.util.KeyValues; import org.olat.core.gui.control.Controller; import org.olat.core.gui.control.Event; @@ -45,6 +51,14 @@ import org.olat.core.gui.control.generic.closablewrapper.CloseableModalControlle import org.olat.core.id.Identity; import org.olat.core.util.CodeHelper; import org.olat.core.util.StringHelper; +import org.olat.core.util.ValidationStatus; +import org.olat.core.util.WebappHelper; +import org.olat.core.util.vfs.LocalFolderImpl; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.VFSManager; +import org.olat.core.util.vfs.filters.VFSSystemItemFilter; import org.olat.group.BusinessGroup; import org.olat.modules.bigbluebutton.BigBlueButtonDispatcher; import org.olat.modules.bigbluebutton.BigBlueButtonManager; @@ -55,6 +69,7 @@ import org.olat.modules.bigbluebutton.BigBlueButtonModule; import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsPublishingEnum; import org.olat.modules.bigbluebutton.BigBlueButtonServer; import org.olat.modules.bigbluebutton.BigBlueButtonTemplatePermissions; +import org.olat.modules.bigbluebutton.manager.SlidesContainerMapper; import org.olat.repository.RepositoryEntry; import org.olat.user.UserManager; import org.springframework.beans.factory.annotation.Autowired; @@ -88,6 +103,8 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { private TextElement externalLinkEl; private MultipleSelectionElement passwordEnableEl; private TextElement passwordEl; + private FileElement uploadSlidesEl; + private FormLayoutContainer slidesCont; private final Mode mode; private final String subIdent; @@ -97,6 +114,12 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { private BigBlueButtonMeeting meeting; private final List<BigBlueButtonTemplatePermissions> permissions; private List<BigBlueButtonMeetingTemplate> templates; + private SlidesContainerMapper slidesMapper; + private final String mapperUri; + private VFSContainer slidesContainer; + private VFSContainer temporaryContainer; + private List<SlideWrapper> documentWrappers = new ArrayList<>(); + private int count = 0; private final boolean running; private final boolean editable; @@ -129,8 +152,13 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { this.businessGroup = businessGroup; this.permissions = permissions; templates = bigBlueButtonManager.getTemplates(); + + temporaryContainer = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID())); + slidesMapper = new SlidesContainerMapper(temporaryContainer); + mapperUri = registerCacheableMapper(null, null, slidesMapper); initForm(ureq); + reloadSlides(); } public EditBigBlueButtonMeetingController(UserRequest ureq, WindowControl wControl, @@ -149,8 +177,14 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { editable = isEditable(meeting, ureq); editableInternal = isEditableInternal(meeting, ureq); administrator = ureq.getUserSession().getRoles().isAdministrator(); + + slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting); + temporaryContainer = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID())); + slidesMapper = new SlidesContainerMapper(temporaryContainer, slidesContainer); + mapperUri = registerCacheableMapper(null, null, slidesMapper); initForm(ureq); + reloadSlides(); } private boolean isEditable(BigBlueButtonMeeting m, UserRequest ureq) { @@ -209,6 +243,18 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { mainPresenterEl.setElementCssClass("o_sel_bbb_edit_meeting_presenter"); mainPresenterEl.setEnabled(editable || editableInternal); + // slides + String page = velocity_root + "/meeting_slides.html"; + slidesCont = FormLayoutContainer.createCustomFormLayout("meeting.slides.container", getTranslator(), page); + slidesCont.setLabel("meeting.slides", null); + slidesCont.contextPut("mapperUri", mapperUri); + formLayout.add(slidesCont); + + uploadSlidesEl = uifactory.addFileElement(getWindowControl(), "meeting.slides.upload", "meeting.slides", formLayout); + uploadSlidesEl.addActionListener(FormEvent.ONCHANGE); + uploadSlidesEl.setVisible(editable || editableInternal); + uploadSlidesEl.limitToMimeType(BigBlueButtonModule.SLIDES_MIME_TYPES, "error.slides.type", null); + Long selectedTemplateKey = meeting == null || meeting.getTemplate() == null ? null : meeting.getTemplate().getKey(); KeyValues templatesKeyValues = new KeyValues(); @@ -385,9 +431,58 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { listenTo(cmc); } + private void reloadSlides() { + Map<String,SlideWrapper> docsMap = documentWrappers.stream() + .collect(Collectors.toMap(SlideWrapper::getFilename, doc -> doc)); + + List<SlideWrapper> wrappers = new ArrayList<>(); + if(temporaryContainer != null && temporaryContainer.exists()) { + reloadSlides(temporaryContainer, true, wrappers, docsMap); + } + if(slidesContainer != null && slidesContainer.exists()) { + reloadSlides(slidesContainer, false, wrappers, docsMap); + } + + slidesCont.contextPut("documents", wrappers); + slidesCont.setVisible(!wrappers.isEmpty()); + + if(uploadSlidesEl != null && uploadSlidesEl.isVisible()) { + if(wrappers.isEmpty()) { + uploadSlidesEl.setLabel("meeting.slides.upload", null); + slidesCont.setDirty(true); + } else { + uploadSlidesEl.setLabel(null, null); + } + } + } + + private void reloadSlides(VFSContainer container, boolean temporary, List<SlideWrapper> wrappers, Map<String,SlideWrapper> docsMap) { + List<VFSItem> documents = container.getItems(new VFSSystemItemFilter()); + for (VFSItem document : documents) { + if(document instanceof VFSLeaf) { + SlideWrapper wrapper = docsMap.get(document.getName()); + if(wrapper == null) { + wrapper = new SlideWrapper((VFSLeaf)document, temporary); + documentWrappers.add(wrapper); + if(editable || editableInternal) { + FormLink deleteButton = uifactory + .addFormLink("delete_" + (++count), "delete", "delete", null, slidesCont, Link.BUTTON_XSMALL); + deleteButton.setUserObject(wrapper); + wrapper.setDeleteButton(deleteButton); + } + } + if(!wrapper.isDeleted()) { + wrappers.add(wrapper); + } + } + } + } + @Override protected void doDispose() { - // + if(temporaryContainer != null) { + temporaryContainer.deleteSilently(); + } } @Override @@ -465,10 +560,31 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { allOk &= false; } + allOk &= validateSlidesSize(); + return allOk; } - + private boolean validateSlidesSize() { + boolean allOk = true; + + Integer maxSizeInMb = bigBlueButtonModule.getMaxUploadSize(); + slidesCont.clearError(); + if(maxSizeInMb != null && maxSizeInMb.intValue() > 0) { + long total = 0l; + for(SlideWrapper doc:documentWrappers) { + if(!doc.isDeleted()) { + total += doc.getDocument().getSize(); + } + } + if(total > (maxSizeInMb.intValue() * 1000 * 1000)) { + slidesCont.setErrorKey("error.slides.size", new String[] { maxSizeInMb.toString() }); + allOk &= false; + } + } + + return allOk; + } @Override protected void event(UserRequest ureq, Controller source, Event event) { @@ -502,6 +618,19 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { BigBlueButtonUIHelper.validateReadableIdentifier(externalLinkEl, meeting); } else if (serverEl == source) { serverChangeWarning(); + } else if(uploadSlidesEl == source) { + if(uploadSlidesEl.getUploadFile() != null && StringHelper.containsNonWhitespace(uploadSlidesEl.getUploadFileName())) { + doUploadSlide(uploadSlidesEl.getUploadFile(), uploadSlidesEl.getUploadFileName()); + reloadSlides(); + validateSlidesSize(); + uploadSlidesEl.reset(); + } + } else if(source instanceof FormLink) { + FormLink link = (FormLink)source; + if("delete".equals(link.getCmd()) && link.getUserObject() instanceof SlideWrapper) { + doDeleteSlide((SlideWrapper)link.getUserObject()); + reloadSlides(); + } } super.formInnerEvent(ureq, source, event); } @@ -587,11 +716,48 @@ public class EditBigBlueButtonMeetingController extends FormBasicController { BigBlueButtonServer server = bigBlueButtonManager.getServer(Long.valueOf(selectedServerKey)); meeting.setServer(server); } + + // copy the slides, eventually update the directory field + doCopySlides(); meeting = bigBlueButtonManager.updateMeeting(meeting); fireEvent(ureq, Event.DONE_EVENT); } + + private void doCopySlides() { + if(documentWrappers.isEmpty()) return; + + VFSContainer storage = bigBlueButtonManager.getSlidesContainer(meeting); + for(SlideWrapper doc:documentWrappers) { + if(doc.isDeleted()) { + doc.getDocument().deleteSilently(); + } else if(doc.isTemporary()) { + VFSLeaf target = storage.createChildLeaf(doc.getFilename()); + VFSManager.copyContent(doc.getDocument(), target, true); + } + } + } + + private void doUploadSlide(File file, String filename) { + List<ValidationStatus> validationResults = new ArrayList<>(); + uploadSlidesEl.validate(validationResults); + if(validationResults.isEmpty()) { + VFSLeaf newSlide = VFSManager.resolveOrCreateLeafFromPath(temporaryContainer, filename); + VFSManager.copyContent(file, newSlide); + + for(SlideWrapper doc:documentWrappers) { + if(filename.equals(doc.getFilename())) { + doc.setDeleted(false); + } + } + } + } + + private void doDeleteSlide(SlideWrapper slide) { + slide.setDeleted(true); + slidesCont.setDirty(true); + } @Override protected void formCancelled(UserRequest ureq) { diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/SlideUploadController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/SlideUploadController.java new file mode 100644 index 0000000000000000000000000000000000000000..685809d28c37c504b68fc42120098945f0be4b88 --- /dev/null +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/SlideUploadController.java @@ -0,0 +1,135 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.bigbluebutton.ui; + +import java.util.ArrayList; +import java.util.List; + +import org.olat.core.commons.persistence.DB; +import org.olat.core.gui.UserRequest; +import org.olat.core.gui.components.form.flexible.FormItemContainer; +import org.olat.core.gui.components.form.flexible.elements.FileElement; +import org.olat.core.gui.components.form.flexible.impl.FormBasicController; +import org.olat.core.gui.components.form.flexible.impl.FormEvent; +import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer; +import org.olat.core.gui.control.Controller; +import org.olat.core.gui.control.Event; +import org.olat.core.gui.control.WindowControl; +import org.olat.core.util.ValidationStatus; +import org.olat.core.util.vfs.VFSContainer; +import org.olat.core.util.vfs.VFSItem; +import org.olat.core.util.vfs.VFSLeaf; +import org.olat.core.util.vfs.filters.VFSSystemItemFilter; +import org.olat.modules.bigbluebutton.BigBlueButtonManager; +import org.olat.modules.bigbluebutton.BigBlueButtonMeeting; +import org.olat.modules.bigbluebutton.BigBlueButtonModule; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * Initial date: 24 déc. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SlideUploadController extends FormBasicController { + + private FileElement fileEl; + + private BigBlueButtonMeeting meeting; + + @Autowired + private DB dbInstance; + @Autowired + private BigBlueButtonModule bigBlueButtonModule; + @Autowired + private BigBlueButtonManager bigBlueButtonManager; + + public SlideUploadController(UserRequest ureq, WindowControl wControl, BigBlueButtonMeeting meeting) { + super(ureq, wControl); + this.meeting = meeting; + initForm(ureq); + } + + @Override + protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) { + formLayout.setElementCssClass("o_sel_course_gta_upload_form"); + + fileEl = uifactory.addFileElement(getWindowControl(), "file", "meeting.slides", formLayout); + fileEl.setMandatory(true); + fileEl.addActionListener(FormEvent.ONCHANGE); + fileEl.limitToMimeType(BigBlueButtonModule.SLIDES_MIME_TYPES, "error.slides.type", null); + + FormLayoutContainer buttonCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator()); + buttonCont.setRootForm(mainForm); + formLayout.add(buttonCont); + uifactory.addFormCancelButton("cancel", buttonCont, ureq, getWindowControl()); + uifactory.addFormSubmitButton("save", buttonCont); + } + + @Override + protected void doDispose() { + // + } + + @Override + protected boolean validateFormLogic(UserRequest ureq) { + boolean allOk = super.validateFormLogic(ureq); + + fileEl.clearError(); + List<ValidationStatus> validationResults = new ArrayList<>(); + fileEl.validate(validationResults); + if(validationResults.isEmpty()) { + Integer maxSizeInMb = bigBlueButtonModule.getMaxUploadSize(); + if(maxSizeInMb != null && maxSizeInMb.intValue() > 0) { + long total = fileEl.getUploadSize(); + VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting); + List<VFSItem> documents = slidesContainer.getItems(new VFSSystemItemFilter()); + for(VFSItem doc:documents) { + if(doc instanceof VFSLeaf) { + total += ((VFSLeaf)doc).getSize(); + } + } + + if(total > (maxSizeInMb.intValue() * 1000 * 1000)) { + fileEl.setErrorKey("error.slides.size", new String[] { maxSizeInMb.toString() }); + allOk &= false; + } + } + } else { + allOk &= false; + } + + return allOk; + } + + @Override + protected void formOK(UserRequest ureq) { + VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting); + fileEl.moveUploadFileTo(slidesContainer); + meeting = bigBlueButtonManager.updateMeeting(meeting); + dbInstance.commit(); + fireEvent(ureq, Event.DONE_EVENT); + } + + @Override + protected void formCancelled(UserRequest ureq) { + fireEvent(ureq, Event.CANCELLED_EVENT); + } +} \ No newline at end of file diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/SlideWrapper.java b/src/main/java/org/olat/modules/bigbluebutton/ui/SlideWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7e6cd69ac6dadb24e8bd54ddb13e7600b7b79655 --- /dev/null +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/SlideWrapper.java @@ -0,0 +1,77 @@ +/** + * <a href="http://www.openolat.org"> + * OpenOLAT - Online Learning and Training</a><br> + * <p> + * Licensed under the Apache License, Version 2.0 (the "License"); <br> + * you may not use this file except in compliance with the License.<br> + * You may obtain a copy of the License at the + * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> + * <p> + * Unless required by applicable law or agreed to in writing,<br> + * software distributed under the License is distributed on an "AS IS" BASIS, <br> + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> + * See the License for the specific language governing permissions and <br> + * limitations under the License. + * <p> + * Initial code contributed and copyrighted by<br> + * frentix GmbH, http://www.frentix.com + * <p> + */ +package org.olat.modules.bigbluebutton.ui; + +import org.olat.core.gui.components.form.flexible.elements.FormLink; +import org.olat.core.util.Formatter; +import org.olat.core.util.vfs.VFSLeaf; + +/** + * + * Initial date: 23 déc. 2020<br> + * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com + * + */ +public class SlideWrapper { + + private final VFSLeaf document; + private FormLink deleteButton; + private boolean deleted = false; + private boolean temporary = false; + + public SlideWrapper(VFSLeaf document, boolean temporary) { + this.document = document; + this.temporary = temporary; + } + + public String getFilename() { + return document.getName(); + } + + public boolean isTemporary() { + return temporary; + } + + public String getLabel() { + return document.getName() + " (" + Formatter.formatBytes(document.getSize()) + ")"; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public VFSLeaf getDocument() { + return document; + } + + public FormLink getDeleteButton() { + return deleteButton; + } + + public void setDeleteButton(FormLink deleteButton) { + this.deleteButton = deleteButton; + deleteButton.setUserObject(this); + } + +} diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting.html b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting.html index c01dd7435d2ac909feceb7dac473966e1e648da9..b04e8abb20aa52d53239fa409b6b3a42d59db681 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting.html +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting.html @@ -30,6 +30,18 @@ </div> #end +#if($r.isNotEmpty($documents)) +<label forid="meeting-slides"><i class="o_icon o_filetype_file"> </i> $r.translate("meeting.slides")</label> +<ul id="meeting-slides" class="list-unstyled o_slides"> + #foreach($document in $documents) + <li><a href="$mapperUri/slides/$r.encodeUrl($document.filename)" target="_blank"><i class="o_icon o_icon-fw $r.getFiletypeIconCss($document.filename)"> </i> $r.escapeHtml($document.label)</a> #if($r.visible($document.deleteButton))$r.render($document.deleteButton)#end</li> + #end +</ul> +#end +#if($r.available("meeting.slides.upload")) + $r.render("meeting.slides.upload") +#end + #if($r.isTrue($ended)) <div class="o_block_large o_warning">$r.translate("meeting.ended")</div> #end diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting_slides.html b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting_slides.html new file mode 100644 index 0000000000000000000000000000000000000000..00e2aa009664851207a6784d479446fea5ba89dc --- /dev/null +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_content/meeting_slides.html @@ -0,0 +1,9 @@ +#if($r.isNotEmpty($documents)) +<ul class="form-static list-unstyled o_slides"> + #foreach($document in $documents) + #if($r.isFalse($document.deleted)) + <li><a href="$mapperUri/slides/$r.encodeUrl($document.filename)" target="_blank"><i class="o_icon o_icon-fw $r.getFiletypeIconCss($document.filename)"> </i> $r.escapeHtml($document.label)</a> #if($r.visible($document.deleteButton))$r.render($document.deleteButton)#end</li> + #end + #end +</ul> +#end \ No newline at end of file diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties index 939511dbb240a381720c0ccd93a81d8d0515cc5a..a2f992faca377134b382310612d9a1729462f2c7 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_de.properties @@ -50,6 +50,8 @@ error.end.past=Der Online-Termin kann nicht in der Vergangenheit geplant werden. error.first.date.in.past=Der erste Termin kann sich nicht in der Vergangenheit befinden. error.identifier.in.use=Name ist schon verwendet. Bitte einen anderen w\u00e4hlen. error.identifier.url.not.valid=Die URL ist nicht g\u00fcltig. Bitte Sonderzeichen wie $, ?, Leerschl\u00e4ge entfernen. +error.slides.size=Total Gr\u00f6sse den Pr\u00e4sentationsfolien muss kleiner als {0}MB sein. +error.slides.type=Pr\u00e4sentationsfolien sind limitiert zu Office und PDF Dokumenten und Bilder. error.password=Passwort ist falsch. error.prefix=Ein Fehler ist aufgetreten\: error.same.day=Sie haben schon ein Meeting an diesem Tag geplant. @@ -104,6 +106,8 @@ meeting.recurring.start=Start wiederkehrendes Datum meeting.resource=Kontext meeting.server=Bevorzugter Server meeting.server.auto=Automatisch (beste Performance) +meeting.slides=Pr\u00e4sentationsfolien +meeting.slides.upload=Pr\u00e4sentationsfolien hochladen meeting.start=Beginn meeting.start.button=Online-Termin starten meeting.template=Raumvorlage @@ -149,6 +153,7 @@ servers.title=Server server.status.available=Verf\u00fcgbar server.status.offline=Scheint offline zu sein server.status.disabled=Abgeschaltet +slides.upload.limit=Max. Grösse den Pr\u00e4sentationsfolien (in MB) table.header.available=Verf\u00fcgbarkeit table.header.breakout.meetings=\# Breakout table.header.breakout.recording.meetings=\# Breakout Recording diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties index f707ee5d73b7b7aa1d7e75e6fc9b846174c439e1..7ddfca48a942f9f837433bf7f83b36a0c43b4acf 100644 --- a/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties +++ b/src/main/java/org/olat/modules/bigbluebutton/ui/_i18n/LocalStrings_en.properties @@ -56,6 +56,8 @@ error.same.day=You already have a meeting planed at this date. error.server.exists=A server with this URL already exists. error.server.raw={1} <small>Key {0}</small> error.serverDisabled=Server is currently not available. +error.slides.size=Total size of the slides needs to be smaller than {0}MB. +error.slides.type=Slides are limited to office and PDF documents and images. error.start.after.end=The end date must not be before the start date. error.too.long.time=Time is too long. It is limited to {0} minutes. error.url.invalid=Invalid server URL @@ -104,6 +106,8 @@ meeting.recurring.start=Start recurring date meeting.resource=Context meeting.server=Preferred server meeting.server.auto=Automatically (best performance) +meeting.slides=Slides +meeting.slides.upload=Upload slides meeting.start=Start date meeting.start.button=Start the online-meeting meeting.template=Room-template @@ -149,6 +153,7 @@ server.status.available=Available server.status.offline=Seems to be offline server.status.disabled=Disabled servers.title=Servers +slides.upload.limit=Max. size of slides (in MB) table.header.available=Availability table.header.breakout.meetings=\# Breakout table.header.breakout.recording.meetings=\# Breakout Recording diff --git a/src/main/resources/database/mysql/alter_15_3_x_to_15_3_8.sql b/src/main/resources/database/mysql/alter_15_3_x_to_15_3_8.sql index 9599fc8ee828769a654778541ec5b11334ff0bb3..9587559c727e9cdaf0bbdbfb7743115d4f850ccd 100644 --- a/src/main/resources/database/mysql/alter_15_3_x_to_15_3_8.sql +++ b/src/main/resources/database/mysql/alter_15_3_x_to_15_3_8.sql @@ -1,3 +1,5 @@ -- BigBlueButton alter table o_bbb_meeting add column b_password varchar(64) default null; +alter table o_bbb_meeting add column b_directory varchar(64) default null; +alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory); diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql index a6b8ea2e3c7599f372ed86a125d5617774d308a8..3761aa106cf17cb68cfc106ba8858adc441d020b 100644 --- a/src/main/resources/database/mysql/setupDatabase.sql +++ b/src/main/resources/database/mysql/setupDatabase.sql @@ -1225,6 +1225,7 @@ create table o_bbb_meeting ( b_followuptime bigint default 0 not null, b_end_with_followuptime datetime, b_main_presenter varchar(255), + b_directory varchar(64) default null, b_recordings_publishing varchar(16) default 'auto', b_record bool default null, fk_creator_id bigint default null, @@ -3884,6 +3885,7 @@ alter table o_bbb_meeting add constraint bbb_meet_grp_idx foreign key (fk_group_ alter table o_bbb_meeting add constraint bbb_meet_template_idx foreign key (fk_template_id) references o_bbb_template (id); alter table o_bbb_meeting add constraint bbb_meet_creator_idx foreign key (fk_creator_id) references o_bs_identity (id); alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_server_id) references o_bbb_server (id); +alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory); alter table o_bbb_attendee add constraint bbb_attend_ident_idx foreign key (fk_identity_id) references o_bs_identity (id); alter table o_bbb_attendee add constraint bbb_attend_meet_idx foreign key (fk_meeting_id) references o_bbb_meeting (id); diff --git a/src/main/resources/database/oracle/alter_15_3_x_to_15_3_8.sql b/src/main/resources/database/oracle/alter_15_3_x_to_15_3_8.sql index 7e344ad046f0c88ebd214298d4eef271dccb8e2f..65ea60c3d100da045d0902317ef3182dbea8db13 100644 --- a/src/main/resources/database/oracle/alter_15_3_x_to_15_3_8.sql +++ b/src/main/resources/database/oracle/alter_15_3_x_to_15_3_8.sql @@ -1,3 +1,5 @@ -- BigBlueButton alter table o_bbb_meeting add b_password varchar2(64) default null; +alter table o_bbb_meeting add b_directory varchar2(64) default null; +alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory); diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql index 4624c197e40d86ab91f197454594ade6cbd90280..8b52bea73b300b66e48604a1a44633f60edf2e3c 100644 --- a/src/main/resources/database/oracle/setupDatabase.sql +++ b/src/main/resources/database/oracle/setupDatabase.sql @@ -1289,6 +1289,7 @@ create table o_bbb_meeting ( b_followuptime number(20) default 0 not null, b_end_with_followuptime timestamp, b_main_presenter varchar2(255), + b_directory varchar2(64) default null, b_recordings_publishing varchar2(16) default 'auto', b_record number default null, fk_creator_id number(20), @@ -3900,6 +3901,7 @@ alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_serve create index idx_bbb_meet_serv_idx on o_bbb_meeting(fk_server_id); alter table o_bbb_meeting add constraint bbb_meet_creator_idx foreign key (fk_creator_id) references o_bs_identity (id); create index idx_bbb_meet_creator_idx on o_bbb_meeting(fk_creator_id); +alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory); alter table o_bbb_attendee add constraint bbb_attend_ident_idx foreign key (fk_identity_id) references o_bs_identity (id); create index idx_bbb_attend_ident_idx on o_bbb_attendee(fk_identity_id); diff --git a/src/main/resources/database/postgresql/alter_15_3_x_to_15_3_8.sql b/src/main/resources/database/postgresql/alter_15_3_x_to_15_3_8.sql index 9599fc8ee828769a654778541ec5b11334ff0bb3..9587559c727e9cdaf0bbdbfb7743115d4f850ccd 100644 --- a/src/main/resources/database/postgresql/alter_15_3_x_to_15_3_8.sql +++ b/src/main/resources/database/postgresql/alter_15_3_x_to_15_3_8.sql @@ -1,3 +1,5 @@ -- BigBlueButton alter table o_bbb_meeting add column b_password varchar(64) default null; +alter table o_bbb_meeting add column b_directory varchar(64) default null; +alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory); diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql index 65898a4e216c8c45a91a1e052f2a6cf8931f7331..5895c609db930f377c2322065fa6b11cc30b7c1a 100644 --- a/src/main/resources/database/postgresql/setupDatabase.sql +++ b/src/main/resources/database/postgresql/setupDatabase.sql @@ -1247,6 +1247,7 @@ create table o_bbb_meeting ( b_followuptime bigint default 0 not null, b_end_with_followuptime timestamp, b_main_presenter varchar(255), + b_directory varchar(64) default null, b_recordings_publishing varchar(16) default 'auto', b_record bool default null, fk_creator_id int8, @@ -3794,6 +3795,7 @@ alter table o_bbb_meeting add constraint bbb_meet_serv_idx foreign key (fk_serve create index idx_bbb_meet_serv_idx on o_bbb_meeting(fk_server_id); alter table o_bbb_meeting add constraint bbb_meet_creator_idx foreign key (fk_creator_id) references o_bs_identity (id); create index idx_bbb_meet_creator_idx on o_bbb_meeting(fk_creator_id); +alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory); alter table o_bbb_attendee add constraint bbb_attend_ident_idx foreign key (fk_identity_id) references o_bs_identity (id); create index idx_bbb_attend_ident_idx on o_bbb_attendee(fk_identity_id);