/** * BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ * * Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). * * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation; either version 3.0 of the License, or (at your option) any later * version. * * BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along * with BigBlueButton; if not, see <http://www.gnu.org/licenses/>. * */ package org.bigbluebutton.api; import java.io.File; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.io.FileUtils; import org.bigbluebutton.api.domain.Recording; import org.bigbluebutton.api.domain.RecordingMetadata; import org.bigbluebutton.api.util.RecordingMetadataReaderHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RecordingService { private static Logger log = LoggerFactory.getLogger(RecordingService.class); private String processDir = "/var/bigbluebutton/recording/process"; private String publishedDir = "/var/bigbluebutton/published"; private String unpublishedDir = "/var/bigbluebutton/unpublished"; private String deletedDir = "/var/bigbluebutton/deleted"; private RecordingServiceHelper recordingServiceHelper; private String recordStatusDir; public void startIngestAndProcessing(String meetingId) { String done = recordStatusDir + "/" + meetingId + ".done"; File doneFile = new File(done); if (!doneFile.exists()) { try { doneFile.createNewFile(); if (!doneFile.exists()) log.error("Failed to create " + done + " file."); } catch (IOException e) { log.error("Failed to create " + done + " file."); } } else { log.error(done + " file already exists."); } } public List<RecordingMetadata> getRecordingsMetadata(List<String> recordIDs, List<String> states) { List<RecordingMetadata> recs = new ArrayList<RecordingMetadata>(); Map<String, List<File>> allDirectories = getAllDirectories(states); if (recordIDs.isEmpty()) { for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) { recordIDs.addAll(getAllRecordingIds(entry.getValue())); } } for (String recordID : recordIDs) { for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) { List<File> _recs = getRecordingsForPath(recordID, entry.getValue()); Iterator<File> iterator = _recs.iterator(); while (iterator.hasNext()) { RecordingMetadata r = getRecordingMetadata(iterator.next()); if (r != null) { recs.add(r); } } } } return recs; } private static RecordingMetadata getRecordingMetadata(File dir) { File file = new File(dir.getPath() + File.separatorChar + "metadata.xml"); RecordingMetadata rec = RecordingMetadataReaderHelper.getRecordingMetadata(file); return rec; } public List<Recording> getRecordings(List<String> recordIDs, List<String> states) { List<Recording> recs = new ArrayList<Recording>(); Map<String, List<File>> allDirectories = getAllDirectories(states); if (recordIDs.isEmpty()) { for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) { recordIDs.addAll(getAllRecordingIds(entry.getValue())); } } for (String recordID : recordIDs) { for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) { List<File> _recs = getRecordingsForPath(recordID, entry.getValue()); Iterator<File> iterator = _recs.iterator(); while (iterator.hasNext()) { Recording r = getRecordingInfo(iterator.next()); if (r != null) { recs.add(r); } } } } return recs; } public boolean recordingMatchesMetadata(Recording recording, Map<String, String> metadataFilters) { boolean matchesMetadata = true; for (Map.Entry<String, String> filter : metadataFilters.entrySet()) { String metadataValue = recording.getMetadata().get(filter.getKey()); if ( metadataValue == null ) { // The recording doesn't have metadata specified matchesMetadata = false; } else { String filterValue = filter.getValue(); if( filterValue.charAt(0) == '%' && filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.contains(filterValue.substring(1, filterValue.length()-1)) ){ // Filter value embraced by two wild cards // AND the filter value is part of the metadata value } else if( filterValue.charAt(0) == '%' && metadataValue.endsWith(filterValue.substring(1, filterValue.length())) ) { // Filter value starts with a wild cards // AND the filter value ends with the metadata value } else if( filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.startsWith(filterValue.substring(0, filterValue.length()-1)) ) { // Filter value ends with a wild cards // AND the filter value starts with the metadata value } else if( metadataValue.equals(filterValue) ) { // Filter value doesnt have wildcards // AND the filter value is the same as metadata value } else { matchesMetadata = false; } } } return matchesMetadata; } public boolean recordingMatchesMetadata(RecordingMetadata recording, Map<String, String> metadataFilters) { boolean matchesMetadata = true; for (Map.Entry<String, String> filter : metadataFilters.entrySet()) { String metadataValue = recording.getMeta().get().get(filter.getKey()); if ( metadataValue == null ) { // The recording doesn't have metadata specified matchesMetadata = false; } else { String filterValue = filter.getValue(); if( filterValue.charAt(0) == '%' && filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.contains(filterValue.substring(1, filterValue.length()-1)) ){ // Filter value embraced by two wild cards // AND the filter value is part of the metadata value } else if( filterValue.charAt(0) == '%' && metadataValue.endsWith(filterValue.substring(1, filterValue.length())) ) { // Filter value starts with a wild cards // AND the filter value ends with the metadata value } else if( filterValue.charAt(filterValue.length()-1) == '%' && metadataValue.startsWith(filterValue.substring(0, filterValue.length()-1)) ) { // Filter value ends with a wild cards // AND the filter value starts with the metadata value } else if( metadataValue.equals(filterValue) ) { // Filter value doesnt have wildcards // AND the filter value is the same as metadata value } else { matchesMetadata = false; } } } return matchesMetadata; } public List<RecordingMetadata> filterRecordingsByMetadata(List<RecordingMetadata> recordings, Map<String, String> metadataFilters) { List<RecordingMetadata> resultRecordings = new ArrayList<RecordingMetadata>(); for (RecordingMetadata entry : recordings) { if (recordingMatchesMetadata(entry, metadataFilters)) resultRecordings.add(entry); } return resultRecordings; } public Map<String, Recording> filterRecordingsByMetadata(Map<String, Recording> recordings, Map<String, String> metadataFilters) { Map<String, Recording> resultRecordings = new HashMap<String, Recording>(); for (Map.Entry<String, Recording> entry : recordings.entrySet()) { if (recordingMatchesMetadata(entry.getValue(), metadataFilters)) resultRecordings.put(entry.getKey(), entry.getValue()); } return resultRecordings; } public boolean existAnyRecording(List<String> idList) { List<String> publishList = getAllRecordingIds(publishedDir); List<String> unpublishList = getAllRecordingIds(unpublishedDir); for (String id : idList) { if (publishList.contains(id) || unpublishList.contains(id)) { return true; } } return false; } private List<String> getAllRecordingIds(String path) { String[] format = getPlaybackFormats(path); return getAllRecordingIds(path, format); } private List<String> getAllRecordingIds(String path, String[] format) { List<String> ids = new ArrayList<String>(); for (int i = 0; i < format.length; i++) { List<File> recordings = getDirectories(path + File.separatorChar + format[i]); for (int f = 0; f < recordings.size(); f++) { if (!ids.contains(recordings.get(f).getName())) ids.add(recordings.get(f).getName()); } } return ids; } private Set<String> getAllRecordingIds(List<File> recs) { Set<String> ids = new HashSet<String>(); Iterator<File> iterator = recs.iterator(); while (iterator.hasNext()) { ids.add(iterator.next().getName()); } return ids; } private List<File> getRecordingsForPath(String id, List<File> recordings) { List<File> recs = new ArrayList<File>(); Iterator<File> iterator = recordings.iterator(); while (iterator.hasNext()) { File rec = iterator.next(); if (rec.getName().startsWith(id)) { recs.add(rec); } } return recs; } private Recording getRecordingInfo(File dir) { Recording rec = recordingServiceHelper.getRecordingInfo(dir); return rec; } private static void deleteRecording(String id, String path) { String[] format = getPlaybackFormats(path); for (int i = 0; i < format.length; i++) { List<File> recordings = getDirectories(path + File.separatorChar + format[i]); for (int f = 0; f < recordings.size(); f++) { if (recordings.get(f).getName().equals(id)) { deleteDirectory(recordings.get(f)); createDirectory(recordings.get(f)); } } } } private static void createDirectory(File directory) { if (!directory.exists()) directory.mkdirs(); } private static void deleteDirectory(File directory) { /** * Go through each directory and check if it's not empty. We need to * delete files inside a directory before a directory can be deleted. **/ File[] files = directory.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteDirectory(files[i]); } else { files[i].delete(); } } // Now that the directory is empty. Delete it. directory.delete(); } private static List<File> getDirectories(String path) { List<File> files = new ArrayList<File>(); try { DirectoryStream<Path> stream = Files.newDirectoryStream(FileSystems.getDefault().getPath(path)); Iterator<Path> iter = stream.iterator(); while (iter.hasNext()) { Path next = iter.next(); files.add(next.toFile()); } stream.close(); } catch (IOException e) { e.printStackTrace(); } return files; } private static String[] getPlaybackFormats(String path) { List<File> dirs = getDirectories(path); String[] formats = new String[dirs.size()]; for (int i = 0; i < dirs.size(); i++) { formats[i] = dirs.get(i).getName(); } return formats; } public void setRecordingStatusDir(String dir) { recordStatusDir = dir; } public void setUnpublishedDir(String dir) { unpublishedDir = dir; } public void setPublishedDir(String dir) { publishedDir = dir; } public void setRecordingServiceHelper(RecordingServiceHelper r) { recordingServiceHelper = r; } private boolean shouldIncludeState(List<String> states, String type) { boolean r = false; if (!states.isEmpty()) { if (states.contains("any")) { r = true; } else { if (type.equals(Recording.STATE_PUBLISHED) && states.contains(Recording.STATE_PUBLISHED)) { r = true; } else if (type.equals(Recording.STATE_UNPUBLISHED) && states.contains(Recording.STATE_UNPUBLISHED)) { r = true; } else if (type.equals(Recording.STATE_DELETED) && states.contains(Recording.STATE_DELETED)) { r = true; } else if (type.equals(Recording.STATE_PROCESSING) && states.contains(Recording.STATE_PROCESSING)) { r = true; } else if (type.equals(Recording.STATE_PROCESSED) && states.contains(Recording.STATE_PROCESSED)) { r = true; } } } else { if (type.equals(Recording.STATE_PUBLISHED) || type.equals(Recording.STATE_UNPUBLISHED)) { r = true; } } return r; } public void changeState(String recordingId, String state) { if (state.equals(Recording.STATE_PUBLISHED)) { // It can only be published if it is unpublished changeState(unpublishedDir, recordingId, state); } else if (state.equals(Recording.STATE_UNPUBLISHED)) { // It can only be unpublished if it is published changeState(publishedDir, recordingId, state); } else if (state.equals(Recording.STATE_DELETED)) { // It can be deleted from any state changeState(publishedDir, recordingId, state); changeState(unpublishedDir, recordingId, state); } } private void changeState(String path, String recordingId, String state) { String[] format = getPlaybackFormats(path); for (int i = 0; i < format.length; i++) { List<File> recordings = getDirectories(path + File.separatorChar + format[i]); for (int f = 0; f < recordings.size(); f++) { if (recordings.get(f).getName().equalsIgnoreCase(recordingId)) { File dest; if (state.equals(Recording.STATE_PUBLISHED)) { dest = new File(publishedDir + File.separatorChar + format[i]); RecordingService.publishRecording(dest, recordingId, recordings.get(f)); } else if (state.equals(Recording.STATE_UNPUBLISHED)) { dest = new File(unpublishedDir + File.separatorChar + format[i]); RecordingService.unpublishRecording(dest, recordingId, recordings.get(f)); } else if (state.equals(Recording.STATE_DELETED)) { dest = new File(deletedDir + File.separatorChar + format[i]); RecordingService.deleteRecording(dest, recordingId, recordings.get(f)); } else { log.debug(String.format("State: %s, is not supported", state)); return; } } } } } public static void publishRecording(File destDir, String recordingId, File recordingDir) { File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recordingDir.getPath()); RecordingMetadata r = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml); if (r != null) { if (!destDir.exists()) destDir.mkdirs(); try { FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId)); r.setState(Recording.STATE_PUBLISHED); r.setPublished(true); File medataXmlFile = RecordingMetadataReaderHelper.getMetadataXmlLocation( destDir.getAbsolutePath() + File.separatorChar + recordingId); // Process the changes by saving the recording into metadata.xml RecordingMetadataReaderHelper.saveRecordingMetadata(medataXmlFile, r); } catch (IOException e) { log.error("Failed to publish recording : " + recordingId, e); } } } public static void unpublishRecording(File destDir, String recordingId, File recordingDir) { File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recordingDir.getPath()); RecordingMetadata r = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml); if (r != null) { if (!destDir.exists()) destDir.mkdirs(); try { FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId)); r.setState(Recording.STATE_UNPUBLISHED); r.setPublished(false); File medataXmlFile = RecordingMetadataReaderHelper.getMetadataXmlLocation( destDir.getAbsolutePath() + File.separatorChar + recordingId); // Process the changes by saving the recording into metadata.xml RecordingMetadataReaderHelper.saveRecordingMetadata(medataXmlFile, r); } catch (IOException e) { log.error("Failed to unpublish recording : " + recordingId, e); } } } public static void deleteRecording(File destDir, String recordingId, File recordingDir) { File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recordingDir.getPath()); RecordingMetadata r = RecordingMetadataReaderHelper.getRecordingMetadata(metadataXml); if (r != null) { if (!destDir.exists()) destDir.mkdirs(); try { FileUtils.moveDirectory(recordingDir, new File(destDir.getPath() + File.separatorChar + recordingId)); r.setState(Recording.STATE_DELETED); r.setPublished(false); File medataXmlFile = RecordingMetadataReaderHelper.getMetadataXmlLocation( destDir.getAbsolutePath() + File.separatorChar + recordingId); // Process the changes by saving the recording into metadata.xml RecordingMetadataReaderHelper.saveRecordingMetadata(medataXmlFile, r); } catch (IOException e) { log.error("Failed to delete recording : " + recordingId, e); } } } private List<File> getAllDirectories(String state) { List<File> allDirectories = new ArrayList<File>(); String dir = getDestinationBaseDirectoryName(state); if ( dir != null ) { String[] formats = getPlaybackFormats(dir); for (int i = 0; i < formats.length; ++i) { allDirectories.addAll(getDirectories(dir + File.separatorChar + formats[i])); } } return allDirectories; } private Map<String, List<File>> getAllDirectories(List<String> states) { Map<String, List<File>> allDirectories = new HashMap<String, List<File>>(); if ( shouldIncludeState(states, Recording.STATE_PUBLISHED) ) { List<File> _allDirectories = getAllDirectories(Recording.STATE_PUBLISHED); allDirectories.put(Recording.STATE_PUBLISHED, _allDirectories); } if ( shouldIncludeState(states, Recording.STATE_UNPUBLISHED) ) { List<File> _allDirectories = getAllDirectories(Recording.STATE_UNPUBLISHED); allDirectories.put(Recording.STATE_UNPUBLISHED, _allDirectories); } if ( shouldIncludeState(states, Recording.STATE_DELETED) ) { List<File> _allDirectories = getAllDirectories(Recording.STATE_DELETED); allDirectories.put(Recording.STATE_DELETED, _allDirectories); } if ( shouldIncludeState(states, Recording.STATE_PROCESSING) ) { List<File> _allDirectories = getAllDirectories(Recording.STATE_PROCESSING); allDirectories.put(Recording.STATE_PROCESSING, _allDirectories); } if ( shouldIncludeState(states, Recording.STATE_PROCESSED) ) { List<File> _allDirectories = getAllDirectories(Recording.STATE_PROCESSED); allDirectories.put(Recording.STATE_PROCESSED, _allDirectories); } return allDirectories; } public void updateMetaParams(List<String> recordIDs, Map<String,String> metaParams) { // Define the directories used to lookup the recording List<String> states = new ArrayList<String>(); states.add(Recording.STATE_PUBLISHED); states.add(Recording.STATE_UNPUBLISHED); states.add(Recording.STATE_DELETED); // Gather all the existent directories based on the states defined for the lookup Map<String, List<File>> allDirectories = getAllDirectories(states); // Retrieve the actual recording from the directories gathered for the lookup for (String recordID : recordIDs) { for (Map.Entry<String, List<File>> entry : allDirectories.entrySet()) { List<File> recs = getRecordingsForPath(recordID, entry.getValue()); // Lookup the target recording Map<String,File> recsIndexed = indexRecordings(recs); if ( recsIndexed.containsKey(recordID) ) { File recFile = recsIndexed.get(recordID); File metadataXml = RecordingMetadataReaderHelper.getMetadataXmlLocation(recFile.getPath()); updateRecordingMetadata(metadataXml, metaParams, metadataXml); } } } return; } public static void updateRecordingMetadata(File srxMetadataXml, Map<String,String> metaParams, File destMetadataXml) { RecordingMetadata rec = RecordingMetadataReaderHelper.getRecordingMetadata(srxMetadataXml); if (rec != null && rec.getMeta() != null) { for (Map.Entry<String,String> meta : metaParams.entrySet()) { if ( !"".equals(meta.getValue()) ) { // As it has a value, if the meta parameter exists update it, otherwise add it rec.getMeta().set(meta.getKey(), meta.getValue()); } else { // As it doesn't have a value, if it exists delete it if ( rec.getMeta().containsKey(meta.getKey()) ) { rec.getMeta().remove(meta.getKey()); } } } // Process the changes by saving the recording into metadata.xml RecordingMetadataReaderHelper.saveRecordingMetadata(destMetadataXml, rec); } } private Map<String,File> indexRecordings(List<File> recs) { Map<String,File> indexedRecs = new HashMap<String,File>(); Iterator<File> iterator = recs.iterator(); while (iterator.hasNext()) { File rec = iterator.next(); indexedRecs.put(rec.getName(), rec); } return indexedRecs; } private String getDestinationBaseDirectoryName(String state) { return getDestinationBaseDirectoryName(state, false); } private String getDestinationBaseDirectoryName(String state, boolean forceDefault) { String baseDir = null; if ( state.equals(Recording.STATE_PROCESSING) || state.equals(Recording.STATE_PROCESSED) ) baseDir = processDir; else if ( state.equals(Recording.STATE_PUBLISHED) ) baseDir = publishedDir; else if ( state.equals(Recording.STATE_UNPUBLISHED) ) baseDir = unpublishedDir; else if ( state.equals(Recording.STATE_DELETED) ) baseDir = deletedDir; else if ( forceDefault ) baseDir = publishedDir; return baseDir; } }