/**
* 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;
}
}