From 3eb4091808735858b01855d298226d239be464af Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 02:37:52 -0400 Subject: move additional classes to new server lib --- .../FileOrganization/EpisodeFileOrganizer.cs | 834 +++++++++++++++++++++ .../FileOrganization/Extensions.cs | 33 + .../FileOrganization/FileOrganizationNotifier.cs | 80 ++ .../FileOrganization/FileOrganizationService.cs | 283 +++++++ .../FileOrganization/NameUtils.cs | 81 ++ .../FileOrganization/OrganizerScheduledTask.cs | 101 +++ .../FileOrganization/TvFolderOrganizer.cs | 210 ++++++ 7 files changed, 1622 insertions(+) create mode 100644 Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs create mode 100644 Emby.Server.Implementations/FileOrganization/Extensions.cs create mode 100644 Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs create mode 100644 Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs create mode 100644 Emby.Server.Implementations/FileOrganization/NameUtils.cs create mode 100644 Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs create mode 100644 Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs (limited to 'Emby.Server.Implementations/FileOrganization') diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs new file mode 100644 index 000000000..3f4ede478 --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -0,0 +1,834 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Emby.Server.Implementations.Library; +using Emby.Server.Implementations.Logging; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Naming.TV; +using EpisodeInfo = MediaBrowser.Controller.Providers.EpisodeInfo; + +namespace Emby.Server.Implementations.FileOrganization +{ + public class EpisodeFileOrganizer + { + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IFileOrganizationService _organizationService; + private readonly IServerConfigurationManager _config; + private readonly IProviderManager _providerManager; + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public EpisodeFileOrganizer(IFileOrganizationService organizationService, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager) + { + _organizationService = organizationService; + _config = config; + _fileSystem = fileSystem; + _logger = logger; + _libraryManager = libraryManager; + _libraryMonitor = libraryMonitor; + _providerManager = providerManager; + } + + public async Task OrganizeEpisodeFile(string path, AutoOrganizeOptions options, bool overwriteExisting, CancellationToken cancellationToken) + { + _logger.Info("Sorting file {0}", path); + + var result = new FileOrganizationResult + { + Date = DateTime.UtcNow, + OriginalPath = path, + OriginalFileName = Path.GetFileName(path), + Type = FileOrganizerType.Episode, + FileSize = _fileSystem.GetFileInfo(path).Length + }; + + try + { + if (_libraryMonitor.IsPathLocked(path)) + { + result.Status = FileSortingStatus.Failure; + result.StatusMessage = "Path is locked by other processes. Please try again later."; + return result; + } + + var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); + var resolver = new EpisodeResolver(namingOptions, new PatternsLogger()); + + var episodeInfo = resolver.Resolve(path, false) ?? + new MediaBrowser.Naming.TV.EpisodeInfo(); + + var seriesName = episodeInfo.SeriesName; + + if (!string.IsNullOrEmpty(seriesName)) + { + var seasonNumber = episodeInfo.SeasonNumber; + + result.ExtractedSeasonNumber = seasonNumber; + + // Passing in true will include a few extra regex's + var episodeNumber = episodeInfo.EpisodeNumber; + + result.ExtractedEpisodeNumber = episodeNumber; + + var premiereDate = episodeInfo.IsByDate ? + new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo.Day.Value) : + (DateTime?)null; + + if (episodeInfo.IsByDate || (seasonNumber.HasValue && episodeNumber.HasValue)) + { + if (episodeInfo.IsByDate) + { + _logger.Debug("Extracted information from {0}. Series name {1}, Date {2}", path, seriesName, premiereDate.Value); + } + else + { + _logger.Debug("Extracted information from {0}. Series name {1}, Season {2}, Episode {3}", path, seriesName, seasonNumber, episodeNumber); + } + + var endingEpisodeNumber = episodeInfo.EndingEpsiodeNumber; + + result.ExtractedEndingEpisodeNumber = endingEpisodeNumber; + + await OrganizeEpisode(path, + seriesName, + seasonNumber, + episodeNumber, + endingEpisodeNumber, + premiereDate, + options, + overwriteExisting, + false, + result, + cancellationToken).ConfigureAwait(false); + } + else + { + var msg = string.Format("Unable to determine episode number from {0}", path); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + } + } + else + { + var msg = string.Format("Unable to determine series name from {0}", path); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + } + + var previousResult = _organizationService.GetResultBySourcePath(path); + + if (previousResult != null) + { + // Don't keep saving the same result over and over if nothing has changed + if (previousResult.Status == result.Status && previousResult.StatusMessage == result.StatusMessage && result.Status != FileSortingStatus.Success) + { + return previousResult; + } + } + + await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + result.Status = FileSortingStatus.Failure; + result.StatusMessage = ex.Message; + } + + return result; + } + + public async Task OrganizeWithCorrection(EpisodeFileOrganizationRequest request, AutoOrganizeOptions options, CancellationToken cancellationToken) + { + var result = _organizationService.GetResult(request.ResultId); + + try + { + Series series = null; + + if (request.NewSeriesProviderIds.Count > 0) + { + // We're having a new series here + SeriesInfo seriesRequest = new SeriesInfo(); + seriesRequest.ProviderIds = request.NewSeriesProviderIds; + + var refreshOptions = new MetadataRefreshOptions(_fileSystem); + series = new Series(); + series.Id = Guid.NewGuid(); + series.Name = request.NewSeriesName; + + int year; + if (int.TryParse(request.NewSeriesYear, out year)) + { + series.ProductionYear = year; + } + + var seriesFolderName = series.Name; + if (series.ProductionYear.HasValue) + { + seriesFolderName = string.Format("{0} ({1})", seriesFolderName, series.ProductionYear); + } + + series.Path = Path.Combine(request.TargetFolder, seriesFolderName); + + series.ProviderIds = request.NewSeriesProviderIds; + + await series.RefreshMetadata(refreshOptions, cancellationToken); + } + + if (series == null) + { + // Existing Series + series = (Series)_libraryManager.GetItemById(new Guid(request.SeriesId)); + } + + await OrganizeEpisode(result.OriginalPath, + series, + request.SeasonNumber, + request.EpisodeNumber, + request.EndingEpisodeNumber, + null, + options, + true, + request.RememberCorrection, + result, + cancellationToken).ConfigureAwait(false); + + await _organizationService.SaveResult(result, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + result.Status = FileSortingStatus.Failure; + result.StatusMessage = ex.Message; + } + + return result; + } + + private Task OrganizeEpisode(string sourcePath, + string seriesName, + int? seasonNumber, + int? episodeNumber, + int? endingEpiosdeNumber, + DateTime? premiereDate, + AutoOrganizeOptions options, + bool overwriteExisting, + bool rememberCorrection, + FileOrganizationResult result, + CancellationToken cancellationToken) + { + var series = GetMatchingSeries(seriesName, result, options); + + if (series == null) + { + var msg = string.Format("Unable to find series in library matching name {0}", seriesName); + result.Status = FileSortingStatus.Failure; + result.StatusMessage = msg; + _logger.Warn(msg); + return Task.FromResult(true); + } + + return OrganizeEpisode(sourcePath, + series, + seasonNumber, + episodeNumber, + endingEpiosdeNumber, + premiereDate, + options, + overwriteExisting, + rememberCorrection, + result, + cancellationToken); + } + + private async Task OrganizeEpisode(string sourcePath, + Series series, + int? seasonNumber, + int? episodeNumber, + int? endingEpiosdeNumber, + DateTime? premiereDate, + AutoOrganizeOptions options, + bool overwriteExisting, + bool rememberCorrection, + FileOrganizationResult result, + CancellationToken cancellationToken) + { + _logger.Info("Sorting file {0} into series {1}", sourcePath, series.Path); + + var originalExtractedSeriesString = result.ExtractedName; + + bool isNew = string.IsNullOrWhiteSpace(result.Id); + + if (isNew) + { + await _organizationService.SaveResult(result, cancellationToken); + } + + if (!_organizationService.AddToInProgressList(result, isNew)) + { + throw new Exception("File is currently processed otherwise. Please try again later."); + } + + try + { + // Proceed to sort the file + var newPath = await GetNewPath(sourcePath, series, seasonNumber, episodeNumber, endingEpiosdeNumber, premiereDate, options.TvOptions, cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(newPath)) + { + var msg = string.Format("Unable to sort {0} because target path could not be determined.", sourcePath); + throw new Exception(msg); + } + + _logger.Info("Sorting file {0} to new path {1}", sourcePath, newPath); + result.TargetPath = newPath; + + var fileExists = _fileSystem.FileExists(result.TargetPath); + var otherDuplicatePaths = GetOtherDuplicatePaths(result.TargetPath, series, seasonNumber, episodeNumber, endingEpiosdeNumber); + + if (!overwriteExisting) + { + if (options.TvOptions.CopyOriginalFile && fileExists && IsSameEpisode(sourcePath, newPath)) + { + var msg = string.Format("File '{0}' already copied to new path '{1}', stopping organization", sourcePath, newPath); + _logger.Info(msg); + result.Status = FileSortingStatus.SkippedExisting; + result.StatusMessage = msg; + return; + } + + if (fileExists) + { + var msg = string.Format("File '{0}' already exists as '{1}', stopping organization", sourcePath, newPath); + _logger.Info(msg); + result.Status = FileSortingStatus.SkippedExisting; + result.StatusMessage = msg; + result.TargetPath = newPath; + return; + } + + if (otherDuplicatePaths.Count > 0) + { + var msg = string.Format("File '{0}' already exists as these:'{1}'. Stopping organization", sourcePath, string.Join("', '", otherDuplicatePaths)); + _logger.Info(msg); + result.Status = FileSortingStatus.SkippedExisting; + result.StatusMessage = msg; + result.DuplicatePaths = otherDuplicatePaths; + return; + } + } + + PerformFileSorting(options.TvOptions, result); + + if (overwriteExisting) + { + var hasRenamedFiles = false; + + foreach (var path in otherDuplicatePaths) + { + _logger.Debug("Removing duplicate episode {0}", path); + + _libraryMonitor.ReportFileSystemChangeBeginning(path); + + var renameRelatedFiles = !hasRenamedFiles && + string.Equals(Path.GetDirectoryName(path), Path.GetDirectoryName(result.TargetPath), StringComparison.OrdinalIgnoreCase); + + if (renameRelatedFiles) + { + hasRenamedFiles = true; + } + + try + { + DeleteLibraryFile(path, renameRelatedFiles, result.TargetPath); + } + catch (IOException ex) + { + _logger.ErrorException("Error removing duplicate episode", ex, path); + } + finally + { + _libraryMonitor.ReportFileSystemChangeComplete(path, true); + } + } + } + } + catch (Exception ex) + { + result.Status = FileSortingStatus.Failure; + result.StatusMessage = ex.Message; + _logger.Warn(ex.Message); + return; + } + finally + { + _organizationService.RemoveFromInprogressList(result); + } + + if (rememberCorrection) + { + SaveSmartMatchString(originalExtractedSeriesString, series, options); + } + } + + private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options) + { + if (string.IsNullOrEmpty(matchString) || matchString.Length < 3) + { + return; + } + + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase)); + + if (info == null) + { + info = new SmartMatchInfo(); + info.ItemName = series.Name; + info.OrganizerType = FileOrganizerType.Episode; + info.DisplayName = series.Name; + var list = options.SmartMatchInfos.ToList(); + list.Add(info); + options.SmartMatchInfos = list.ToArray(); + } + + if (!info.MatchStrings.Contains(matchString, StringComparer.OrdinalIgnoreCase)) + { + var list = info.MatchStrings.ToList(); + list.Add(matchString); + info.MatchStrings = list.ToArray(); + _config.SaveAutoOrganizeOptions(options); + } + } + + private void DeleteLibraryFile(string path, bool renameRelatedFiles, string targetPath) + { + _fileSystem.DeleteFile(path); + + if (!renameRelatedFiles) + { + return; + } + + // Now find other files + var originalFilenameWithoutExtension = Path.GetFileNameWithoutExtension(path); + var directory = Path.GetDirectoryName(path); + + if (!string.IsNullOrWhiteSpace(originalFilenameWithoutExtension) && !string.IsNullOrWhiteSpace(directory)) + { + // Get all related files, e.g. metadata, images, etc + var files = _fileSystem.GetFilePaths(directory) + .Where(i => (Path.GetFileNameWithoutExtension(i) ?? string.Empty).StartsWith(originalFilenameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + var targetFilenameWithoutExtension = Path.GetFileNameWithoutExtension(targetPath); + + foreach (var file in files) + { + directory = Path.GetDirectoryName(file); + var filename = Path.GetFileName(file); + + filename = filename.Replace(originalFilenameWithoutExtension, targetFilenameWithoutExtension, + StringComparison.OrdinalIgnoreCase); + + var destination = Path.Combine(directory, filename); + + _fileSystem.MoveFile(file, destination); + } + } + } + + private List GetOtherDuplicatePaths(string targetPath, + Series series, + int? seasonNumber, + int? episodeNumber, + int? endingEpisodeNumber) + { + // TODO: Support date-naming? + if (!seasonNumber.HasValue || !episodeNumber.HasValue) + { + return new List(); + } + + var episodePaths = series.GetRecursiveChildren() + .OfType() + .Where(i => + { + var locationType = i.LocationType; + + // Must be file system based and match exactly + if (locationType != LocationType.Remote && + locationType != LocationType.Virtual && + i.ParentIndexNumber.HasValue && + i.ParentIndexNumber.Value == seasonNumber && + i.IndexNumber.HasValue && + i.IndexNumber.Value == episodeNumber) + { + + if (endingEpisodeNumber.HasValue || i.IndexNumberEnd.HasValue) + { + return endingEpisodeNumber.HasValue && i.IndexNumberEnd.HasValue && + endingEpisodeNumber.Value == i.IndexNumberEnd.Value; + } + + return true; + } + + return false; + }) + .Select(i => i.Path) + .ToList(); + + var folder = Path.GetDirectoryName(targetPath); + var targetFileNameWithoutExtension = _fileSystem.GetFileNameWithoutExtension(targetPath); + + try + { + var filesOfOtherExtensions = _fileSystem.GetFilePaths(folder) + .Where(i => _libraryManager.IsVideoFile(i) && string.Equals(_fileSystem.GetFileNameWithoutExtension(i), targetFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)); + + episodePaths.AddRange(filesOfOtherExtensions); + } + catch (IOException) + { + // No big deal. Maybe the season folder doesn't already exist. + } + + return episodePaths.Where(i => !string.Equals(i, targetPath, StringComparison.OrdinalIgnoreCase)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) + { + _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); + + _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); + + var targetAlreadyExists = _fileSystem.FileExists(result.TargetPath); + + try + { + if (targetAlreadyExists || options.CopyOriginalFile) + { + _fileSystem.CopyFile(result.OriginalPath, result.TargetPath, true); + } + else + { + _fileSystem.MoveFile(result.OriginalPath, result.TargetPath); + } + + result.Status = FileSortingStatus.Success; + result.StatusMessage = string.Empty; + } + catch (Exception ex) + { + var errorMsg = string.Format("Failed to move file from {0} to {1}: {2}", result.OriginalPath, result.TargetPath, ex.Message); + + result.Status = FileSortingStatus.Failure; + result.StatusMessage = errorMsg; + _logger.ErrorException(errorMsg, ex); + + return; + } + finally + { + _libraryMonitor.ReportFileSystemChangeComplete(result.TargetPath, true); + } + + if (targetAlreadyExists && !options.CopyOriginalFile) + { + try + { + _fileSystem.DeleteFile(result.OriginalPath); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); + } + } + } + + private Series GetMatchingSeries(string seriesName, FileOrganizationResult result, AutoOrganizeOptions options) + { + var parsedName = _libraryManager.ParseName(seriesName); + + var yearInName = parsedName.Year; + var nameWithoutYear = parsedName.Name; + + result.ExtractedName = nameWithoutYear; + result.ExtractedYear = yearInName; + + var series = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true + }) + .Cast() + .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i)) + .Where(i => i.Item2 > 0) + .OrderByDescending(i => i.Item2) + .Select(i => i.Item1) + .FirstOrDefault(); + + if (series == null) + { + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(e => e.MatchStrings.Contains(nameWithoutYear, StringComparer.OrdinalIgnoreCase)); + + if (info != null) + { + series = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true, + Name = info.ItemName + + }).Cast().FirstOrDefault(); + } + } + + return series; + } + + /// + /// Gets the new path. + /// + /// The source path. + /// The series. + /// The season number. + /// The episode number. + /// The ending episode number. + /// The premiere date. + /// The options. + /// The cancellation token. + /// System.String. + private async Task GetNewPath(string sourcePath, + Series series, + int? seasonNumber, + int? episodeNumber, + int? endingEpisodeNumber, + DateTime? premiereDate, + TvFileOrganizationOptions options, + CancellationToken cancellationToken) + { + var episodeInfo = new EpisodeInfo + { + IndexNumber = episodeNumber, + IndexNumberEnd = endingEpisodeNumber, + MetadataCountryCode = series.GetPreferredMetadataCountryCode(), + MetadataLanguage = series.GetPreferredMetadataLanguage(), + ParentIndexNumber = seasonNumber, + SeriesProviderIds = series.ProviderIds, + PremiereDate = premiereDate + }; + + var searchResults = await _providerManager.GetRemoteSearchResults(new RemoteSearchQuery + { + SearchInfo = episodeInfo + + }, cancellationToken).ConfigureAwait(false); + + var episode = searchResults.FirstOrDefault(); + + if (episode == null) + { + var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); + _logger.Warn(msg); + throw new Exception(msg); + } + + var episodeName = episode.Name; + + //if (string.IsNullOrWhiteSpace(episodeName)) + //{ + // var msg = string.Format("No provider metadata found for {0} season {1} episode {2}", series.Name, seasonNumber, episodeNumber); + // _logger.Warn(msg); + // return null; + //} + + seasonNumber = seasonNumber ?? episode.ParentIndexNumber; + episodeNumber = episodeNumber ?? episode.IndexNumber; + + var newPath = GetSeasonFolderPath(series, seasonNumber.Value, options); + + // MAX_PATH - trailing charachter - drive component: 260 - 1 - 3 = 256 + // Usually newPath would include the drive component, but use 256 to be sure + var maxFilenameLength = 256 - newPath.Length; + + if (!newPath.EndsWith(@"\")) + { + // Remove 1 for missing backslash combining path and filename + maxFilenameLength--; + } + + // Remove additional 4 chars to prevent PathTooLongException for downloaded subtitles (eg. filename.ext.eng.srt) + maxFilenameLength -= 4; + + var episodeFileName = GetEpisodeFileName(sourcePath, series.Name, seasonNumber.Value, episodeNumber.Value, endingEpisodeNumber, episodeName, options, maxFilenameLength); + + if (string.IsNullOrEmpty(episodeFileName)) + { + // cause failure + return string.Empty; + } + + newPath = Path.Combine(newPath, episodeFileName); + + return newPath; + } + + /// + /// Gets the season folder path. + /// + /// The series. + /// The season number. + /// The options. + /// System.String. + private string GetSeasonFolderPath(Series series, int seasonNumber, TvFileOrganizationOptions options) + { + // If there's already a season folder, use that + var season = series + .GetRecursiveChildren(i => i is Season && i.LocationType == LocationType.FileSystem && i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber) + .FirstOrDefault(); + + if (season != null) + { + return season.Path; + } + + var path = series.Path; + + if (series.ContainsEpisodesWithoutSeasonFolders) + { + return path; + } + + if (seasonNumber == 0) + { + return Path.Combine(path, _fileSystem.GetValidFilename(options.SeasonZeroFolderName)); + } + + var seasonFolderName = options.SeasonFolderPattern + .Replace("%s", seasonNumber.ToString(_usCulture)) + .Replace("%0s", seasonNumber.ToString("00", _usCulture)) + .Replace("%00s", seasonNumber.ToString("000", _usCulture)); + + return Path.Combine(path, _fileSystem.GetValidFilename(seasonFolderName)); + } + + private string GetEpisodeFileName(string sourcePath, string seriesName, int seasonNumber, int episodeNumber, int? endingEpisodeNumber, string episodeTitle, TvFileOrganizationOptions options, int? maxLength) + { + seriesName = _fileSystem.GetValidFilename(seriesName).Trim(); + + if (string.IsNullOrWhiteSpace(episodeTitle)) + { + episodeTitle = string.Empty; + } + else + { + episodeTitle = _fileSystem.GetValidFilename(episodeTitle).Trim(); + } + + var sourceExtension = (Path.GetExtension(sourcePath) ?? string.Empty).TrimStart('.'); + + var pattern = endingEpisodeNumber.HasValue ? options.MultiEpisodeNamePattern : options.EpisodeNamePattern; + + if (string.IsNullOrWhiteSpace(pattern)) + { + throw new Exception("GetEpisodeFileName: Configured episode name pattern is empty!"); + } + + var result = pattern.Replace("%sn", seriesName) + .Replace("%s.n", seriesName.Replace(" ", ".")) + .Replace("%s_n", seriesName.Replace(" ", "_")) + .Replace("%s", seasonNumber.ToString(_usCulture)) + .Replace("%0s", seasonNumber.ToString("00", _usCulture)) + .Replace("%00s", seasonNumber.ToString("000", _usCulture)) + .Replace("%ext", sourceExtension) + .Replace("%en", "%#1") + .Replace("%e.n", "%#2") + .Replace("%e_n", "%#3"); + + if (endingEpisodeNumber.HasValue) + { + result = result.Replace("%ed", endingEpisodeNumber.Value.ToString(_usCulture)) + .Replace("%0ed", endingEpisodeNumber.Value.ToString("00", _usCulture)) + .Replace("%00ed", endingEpisodeNumber.Value.ToString("000", _usCulture)); + } + + result = result.Replace("%e", episodeNumber.ToString(_usCulture)) + .Replace("%0e", episodeNumber.ToString("00", _usCulture)) + .Replace("%00e", episodeNumber.ToString("000", _usCulture)); + + if (maxLength.HasValue && result.Contains("%#")) + { + // Substract 3 for the temp token length (%#1, %#2 or %#3) + int maxRemainingTitleLength = maxLength.Value - result.Length + 3; + string shortenedEpisodeTitle = string.Empty; + + if (maxRemainingTitleLength > 5) + { + // A title with fewer than 5 letters wouldn't be of much value + shortenedEpisodeTitle = episodeTitle.Substring(0, Math.Min(maxRemainingTitleLength, episodeTitle.Length)); + } + + result = result.Replace("%#1", shortenedEpisodeTitle) + .Replace("%#2", shortenedEpisodeTitle.Replace(" ", ".")) + .Replace("%#3", shortenedEpisodeTitle.Replace(" ", "_")); + } + + if (maxLength.HasValue && result.Length > maxLength.Value) + { + // There may be cases where reducing the title length may still not be sufficient to + // stay below maxLength + var msg = string.Format("Unable to generate an episode file name shorter than {0} characters to constrain to the max path limit", maxLength); + throw new Exception(msg); + } + + return result; + } + + private bool IsSameEpisode(string sourcePath, string newPath) + { + try + { + var sourceFileInfo = _fileSystem.GetFileInfo(sourcePath); + var destinationFileInfo = _fileSystem.GetFileInfo(newPath); + + if (sourceFileInfo.Length == destinationFileInfo.Length) + { + return true; + } + } + catch (FileNotFoundException) + { + return false; + } + catch (IOException) + { + return false; + } + + return false; + } + } +} diff --git a/Emby.Server.Implementations/FileOrganization/Extensions.cs b/Emby.Server.Implementations/FileOrganization/Extensions.cs new file mode 100644 index 000000000..506bc0327 --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/Extensions.cs @@ -0,0 +1,33 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.FileOrganization; +using System.Collections.Generic; + +namespace Emby.Server.Implementations.FileOrganization +{ + public static class ConfigurationExtension + { + public static AutoOrganizeOptions GetAutoOrganizeOptions(this IConfigurationManager manager) + { + return manager.GetConfiguration("autoorganize"); + } + public static void SaveAutoOrganizeOptions(this IConfigurationManager manager, AutoOrganizeOptions options) + { + manager.SaveConfiguration("autoorganize", options); + } + } + + public class AutoOrganizeOptionsFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new List + { + new ConfigurationStore + { + Key = "autoorganize", + ConfigurationType = typeof (AutoOrganizeOptions) + } + }; + } + } +} diff --git a/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs new file mode 100644 index 000000000..2a0176547 --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs @@ -0,0 +1,80 @@ +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Threading; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.FileOrganization +{ + /// + /// Class SessionInfoWebSocketListener + /// + class FileOrganizationNotifier : IServerEntryPoint + { + private readonly IFileOrganizationService _organizationService; + private readonly ISessionManager _sessionManager; + private readonly ITaskManager _taskManager; + + public FileOrganizationNotifier(ILogger logger, IFileOrganizationService organizationService, ISessionManager sessionManager, ITaskManager taskManager) + { + _organizationService = organizationService; + _sessionManager = sessionManager; + _taskManager = taskManager; + } + + public void Run() + { + _organizationService.ItemAdded += _organizationService_ItemAdded; + _organizationService.ItemRemoved += _organizationService_ItemRemoved; + _organizationService.ItemUpdated += _organizationService_ItemUpdated; + _organizationService.LogReset += _organizationService_LogReset; + + //_taskManager.TaskCompleted += _taskManager_TaskCompleted; + } + + private void _organizationService_LogReset(object sender, EventArgs e) + { + _sessionManager.SendMessageToAdminSessions("AutoOrganize_LogReset", (FileOrganizationResult)null, CancellationToken.None); + } + + private void _organizationService_ItemUpdated(object sender, GenericEventArgs e) + { + _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemUpdated", e.Argument, CancellationToken.None); + } + + private void _organizationService_ItemRemoved(object sender, GenericEventArgs e) + { + _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemRemoved", e.Argument, CancellationToken.None); + } + + private void _organizationService_ItemAdded(object sender, GenericEventArgs e) + { + _sessionManager.SendMessageToAdminSessions("AutoOrganize_ItemAdded", e.Argument, CancellationToken.None); + } + + //private void _taskManager_TaskCompleted(object sender, TaskCompletionEventArgs e) + //{ + // var taskWithKey = e.Task.ScheduledTask as IHasKey; + // if (taskWithKey != null && taskWithKey.Key == "AutoOrganize") + // { + // _sessionManager.SendMessageToAdminSessions("AutoOrganize_TaskCompleted", (FileOrganizationResult)null, CancellationToken.None); + // } + //} + + public void Dispose() + { + _organizationService.ItemAdded -= _organizationService_ItemAdded; + _organizationService.ItemRemoved -= _organizationService_ItemRemoved; + _organizationService.ItemUpdated -= _organizationService_ItemUpdated; + _organizationService.LogReset -= _organizationService_LogReset; + + //_taskManager.TaskCompleted -= _taskManager_TaskCompleted; + } + + + } +} diff --git a/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs new file mode 100644 index 000000000..4094e6b9b --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -0,0 +1,283 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Events; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.FileOrganization +{ + public class FileOrganizationService : IFileOrganizationService + { + private readonly ITaskManager _taskManager; + private readonly IFileOrganizationRepository _repo; + private readonly ILogger _logger; + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; + private readonly IProviderManager _providerManager; + private readonly ConcurrentDictionary _inProgressItemIds = new ConcurrentDictionary(); + + public event EventHandler> ItemAdded; + public event EventHandler> ItemUpdated; + public event EventHandler> ItemRemoved; + public event EventHandler LogReset; + + public FileOrganizationService(ITaskManager taskManager, IFileOrganizationRepository repo, ILogger logger, ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, IServerConfigurationManager config, IFileSystem fileSystem, IProviderManager providerManager) + { + _taskManager = taskManager; + _repo = repo; + _logger = logger; + _libraryMonitor = libraryMonitor; + _libraryManager = libraryManager; + _config = config; + _fileSystem = fileSystem; + _providerManager = providerManager; + } + + public void BeginProcessNewFiles() + { + _taskManager.CancelIfRunningAndQueue(); + } + + public Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) + { + if (result == null || string.IsNullOrEmpty(result.OriginalPath)) + { + throw new ArgumentNullException("result"); + } + + result.Id = result.OriginalPath.GetMD5().ToString("N"); + + return _repo.SaveResult(result, cancellationToken); + } + + public QueryResult GetResults(FileOrganizationResultQuery query) + { + var results = _repo.GetResults(query); + + foreach (var result in results.Items) + { + result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id); + } + + return results; + } + + public FileOrganizationResult GetResult(string id) + { + var result = _repo.GetResult(id); + + if (result != null) + { + result.IsInProgress = _inProgressItemIds.ContainsKey(result.Id); + } + + return result; + } + + public FileOrganizationResult GetResultBySourcePath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var id = path.GetMD5().ToString("N"); + + return GetResult(id); + } + + public async Task DeleteOriginalFile(string resultId) + { + var result = _repo.GetResult(resultId); + + _logger.Info("Requested to delete {0}", result.OriginalPath); + + if (!AddToInProgressList(result, false)) + { + throw new Exception("Path is currently processed otherwise. Please try again later."); + } + + try + { + _fileSystem.DeleteFile(result.OriginalPath); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting {0}", ex, result.OriginalPath); + } + finally + { + RemoveFromInprogressList(result); + } + + await _repo.Delete(resultId); + + EventHelper.FireEventIfNotNull(ItemRemoved, this, new GenericEventArgs(result), _logger); + } + + private AutoOrganizeOptions GetAutoOrganizeOptions() + { + return _config.GetAutoOrganizeOptions(); + } + + public async Task PerformOrganization(string resultId) + { + var result = _repo.GetResult(resultId); + + if (string.IsNullOrEmpty(result.TargetPath)) + { + throw new ArgumentException("No target path available."); + } + + var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, + _libraryMonitor, _providerManager); + + var organizeResult = await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None) + .ConfigureAwait(false); + + if (organizeResult.Status != FileSortingStatus.Success) + { + throw new Exception(result.StatusMessage); + } + } + + public async Task ClearLog() + { + await _repo.DeleteAll(); + EventHelper.FireEventIfNotNull(LogReset, this, EventArgs.Empty, _logger); + } + + public async Task PerformEpisodeOrganization(EpisodeFileOrganizationRequest request) + { + var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, + _libraryMonitor, _providerManager); + + var result = await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false); + + if (result.Status != FileSortingStatus.Success) + { + throw new Exception(result.StatusMessage); + } + } + + public QueryResult GetSmartMatchInfos(FileOrganizationResultQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var options = GetAutoOrganizeOptions(); + + var items = options.SmartMatchInfos.Skip(query.StartIndex ?? 0).Take(query.Limit ?? Int32.MaxValue).ToArray(); + + return new QueryResult() + { + Items = items, + TotalRecordCount = options.SmartMatchInfos.Length + }; + } + + public void DeleteSmartMatchEntry(string itemName, string matchString) + { + if (string.IsNullOrEmpty(itemName)) + { + throw new ArgumentNullException("itemName"); + } + + if (string.IsNullOrEmpty(matchString)) + { + throw new ArgumentNullException("matchString"); + } + + var options = GetAutoOrganizeOptions(); + + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, itemName)); + + if (info != null && info.MatchStrings.Contains(matchString)) + { + var list = info.MatchStrings.ToList(); + list.Remove(matchString); + info.MatchStrings = list.ToArray(); + + if (info.MatchStrings.Length == 0) + { + var infos = options.SmartMatchInfos.ToList(); + infos.Remove(info); + options.SmartMatchInfos = infos.ToArray(); + } + + _config.SaveAutoOrganizeOptions(options); + } + } + + /// + /// Attempts to add a an item to the list of currently processed items. + /// + /// The result item. + /// Passing true will notify the client to reload all items, otherwise only a single item will be refreshed. + /// True if the item was added, False if the item is already contained in the list. + public bool AddToInProgressList(FileOrganizationResult result, bool isNewItem) + { + if (string.IsNullOrWhiteSpace(result.Id)) + { + result.Id = result.OriginalPath.GetMD5().ToString("N"); + } + + if (!_inProgressItemIds.TryAdd(result.Id, false)) + { + return false; + } + + result.IsInProgress = true; + + if (isNewItem) + { + EventHelper.FireEventIfNotNull(ItemAdded, this, new GenericEventArgs(result), _logger); + } + else + { + EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs(result), _logger); + } + + return true; + } + + /// + /// Removes an item from the list of currently processed items. + /// + /// The result item. + /// True if the item was removed, False if the item was not contained in the list. + public bool RemoveFromInprogressList(FileOrganizationResult result) + { + bool itemValue; + var retval = _inProgressItemIds.TryRemove(result.Id, out itemValue); + + result.IsInProgress = false; + + EventHelper.FireEventIfNotNull(ItemUpdated, this, new GenericEventArgs(result), _logger); + + return retval; + } + + } +} diff --git a/Emby.Server.Implementations/FileOrganization/NameUtils.cs b/Emby.Server.Implementations/FileOrganization/NameUtils.cs new file mode 100644 index 000000000..eb22ca4ea --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/NameUtils.cs @@ -0,0 +1,81 @@ +using MediaBrowser.Model.Extensions; +using MediaBrowser.Controller.Entities; +using System; +using System.Globalization; +using MediaBrowser.Controller.Extensions; + +namespace Emby.Server.Implementations.FileOrganization +{ + public static class NameUtils + { + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + internal static Tuple GetMatchScore(string sortedName, int? year, T series) + where T : BaseItem + { + var score = 0; + + var seriesNameWithoutYear = series.Name; + if (series.ProductionYear.HasValue) + { + seriesNameWithoutYear = seriesNameWithoutYear.Replace(series.ProductionYear.Value.ToString(UsCulture), String.Empty); + } + + if (IsNameMatch(sortedName, seriesNameWithoutYear)) + { + score++; + + if (year.HasValue && series.ProductionYear.HasValue) + { + if (year.Value == series.ProductionYear.Value) + { + score++; + } + else + { + // Regardless of name, return a 0 score if the years don't match + return new Tuple(series, 0); + } + } + } + + return new Tuple(series, score); + } + + + private static bool IsNameMatch(string name1, string name2) + { + name1 = GetComparableName(name1); + name2 = GetComparableName(name2); + + return String.Equals(name1, name2, StringComparison.OrdinalIgnoreCase); + } + + private static string GetComparableName(string name) + { + name = name.RemoveDiacritics(); + + name = " " + name + " "; + + name = name.Replace(".", " ") + .Replace("_", " ") + .Replace(" and ", " ") + .Replace(".and.", " ") + .Replace("&", " ") + .Replace("!", " ") + .Replace("(", " ") + .Replace(")", " ") + .Replace(":", " ") + .Replace(",", " ") + .Replace("-", " ") + .Replace("'", " ") + .Replace("[", " ") + .Replace("]", " ") + .Replace(" a ", String.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(" the ", String.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(" ", String.Empty); + + return name.Trim(); + } + } +} diff --git a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs new file mode 100644 index 000000000..5be7ba7ad --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -0,0 +1,101 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.FileOrganization +{ + public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask + { + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IServerConfigurationManager _config; + private readonly IFileOrganizationService _organizationService; + private readonly IProviderManager _providerManager; + + public OrganizerScheduledTask(ILibraryMonitor libraryMonitor, ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, IServerConfigurationManager config, IFileOrganizationService organizationService, IProviderManager providerManager) + { + _libraryMonitor = libraryMonitor; + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + _config = config; + _organizationService = organizationService; + _providerManager = providerManager; + } + + public string Name + { + get { return "Organize new media files"; } + } + + public string Description + { + get { return "Processes new files available in the configured watch folder."; } + } + + public string Category + { + get { return "Library"; } + } + + private AutoOrganizeOptions GetAutoOrganizeOptions() + { + return _config.GetAutoOrganizeOptions(); + } + + public async Task Execute(CancellationToken cancellationToken, IProgress progress) + { + if (GetAutoOrganizeOptions().TvOptions.IsEnabled) + { + await new TvFolderOrganizer(_libraryManager, _logger, _fileSystem, _libraryMonitor, _organizationService, _config, _providerManager) + .Organize(GetAutoOrganizeOptions(), cancellationToken, progress).ConfigureAwait(false); + } + } + + /// + /// Creates the triggers that define when the task will run + /// + /// IEnumerable{BaseTaskTrigger}. + public IEnumerable GetDefaultTriggers() + { + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} + }; + } + + public bool IsHidden + { + get { return !GetAutoOrganizeOptions().TvOptions.IsEnabled; } + } + + public bool IsEnabled + { + get { return GetAutoOrganizeOptions().TvOptions.IsEnabled; } + } + + public bool IsLogged + { + get { return false; } + } + + public string Key + { + get { return "AutoOrganize"; } + } + } +} diff --git a/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs new file mode 100644 index 000000000..2850c3a61 --- /dev/null +++ b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs @@ -0,0 +1,210 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; + +namespace Emby.Server.Implementations.FileOrganization +{ + public class TvFolderOrganizer + { + private readonly ILibraryMonitor _libraryMonitor; + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + private readonly IFileOrganizationService _organizationService; + private readonly IServerConfigurationManager _config; + private readonly IProviderManager _providerManager; + + public TvFolderOrganizer(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IFileOrganizationService organizationService, IServerConfigurationManager config, IProviderManager providerManager) + { + _libraryManager = libraryManager; + _logger = logger; + _fileSystem = fileSystem; + _libraryMonitor = libraryMonitor; + _organizationService = organizationService; + _config = config; + _providerManager = providerManager; + } + + private bool EnableOrganization(FileSystemMetadata fileInfo, TvFileOrganizationOptions options) + { + var minFileBytes = options.MinFileSizeMb * 1024 * 1024; + + try + { + return _libraryManager.IsVideoFile(fileInfo.FullName) && fileInfo.Length >= minFileBytes; + } + catch (Exception ex) + { + _logger.ErrorException("Error organizing file {0}", ex, fileInfo.Name); + } + + return false; + } + + public async Task Organize(AutoOrganizeOptions options, CancellationToken cancellationToken, IProgress progress) + { + var watchLocations = options.TvOptions.WatchLocations.ToList(); + + var eligibleFiles = watchLocations.SelectMany(GetFilesToOrganize) + .OrderBy(_fileSystem.GetCreationTimeUtc) + .Where(i => EnableOrganization(i, options.TvOptions)) + .ToList(); + + var processedFolders = new HashSet(); + + progress.Report(10); + + if (eligibleFiles.Count > 0) + { + var numComplete = 0; + + foreach (var file in eligibleFiles) + { + var organizer = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, + _libraryMonitor, _providerManager); + + try + { + var result = await organizer.OrganizeEpisodeFile(file.FullName, options, options.TvOptions.OverwriteExistingEpisodes, cancellationToken).ConfigureAwait(false); + if (result.Status == FileSortingStatus.Success && !processedFolders.Contains(file.DirectoryName, StringComparer.OrdinalIgnoreCase)) + { + processedFolders.Add(file.DirectoryName); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error organizing episode {0}", ex, file.FullName); + } + + numComplete++; + double percent = numComplete; + percent /= eligibleFiles.Count; + + progress.Report(10 + 89 * percent); + } + } + + cancellationToken.ThrowIfCancellationRequested(); + progress.Report(99); + + foreach (var path in processedFolders) + { + var deleteExtensions = options.TvOptions.LeftOverFileExtensionsToDelete + .Select(i => i.Trim().TrimStart('.')) + .Where(i => !string.IsNullOrEmpty(i)) + .Select(i => "." + i) + .ToList(); + + if (deleteExtensions.Count > 0) + { + DeleteLeftOverFiles(path, deleteExtensions); + } + + if (options.TvOptions.DeleteEmptyFolders) + { + if (!IsWatchFolder(path, watchLocations)) + { + DeleteEmptyFolders(path); + } + } + } + + progress.Report(100); + } + + /// + /// Gets the files to organize. + /// + /// The path. + /// IEnumerable{FileInfo}. + private List GetFilesToOrganize(string path) + { + try + { + return _fileSystem.GetFiles(path, true) + .ToList(); + } + catch (IOException ex) + { + _logger.ErrorException("Error getting files from {0}", ex, path); + + return new List(); + } + } + + /// + /// Deletes the left over files. + /// + /// The path. + /// The extensions. + private void DeleteLeftOverFiles(string path, IEnumerable extensions) + { + var eligibleFiles = _fileSystem.GetFiles(path, true) + .Where(i => extensions.Contains(i.Extension, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + foreach (var file in eligibleFiles) + { + try + { + _fileSystem.DeleteFile(file.FullName); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting file {0}", ex, file.FullName); + } + } + } + + /// + /// Deletes the empty folders. + /// + /// The path. + private void DeleteEmptyFolders(string path) + { + try + { + foreach (var d in _fileSystem.GetDirectoryPaths(path)) + { + DeleteEmptyFolders(d); + } + + var entries = _fileSystem.GetFileSystemEntryPaths(path); + + if (!entries.Any()) + { + try + { + _logger.Debug("Deleting empty directory {0}", path); + _fileSystem.DeleteDirectory(path, false); + } + catch (UnauthorizedAccessException) { } + catch (IOException) { } + } + } + catch (UnauthorizedAccessException) { } + } + + /// + /// Determines if a given folder path is contained in a folder list + /// + /// The folder path to check. + /// A list of folders. + private bool IsWatchFolder(string path, IEnumerable watchLocations) + { + return watchLocations.Contains(path, StringComparer.OrdinalIgnoreCase); + } + } +} \ No newline at end of file -- cgit v1.2.3 From 406e6cb8132c1b8ade2872d44d7183267dd51ca8 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 11 Nov 2016 14:55:12 -0500 Subject: update portable projects --- Emby.Drawing.ImageMagick/ImageMagickEncoder.cs | 13 +- Emby.Server.Core/Emby.Server.Core.xproj | 1 + Emby.Server.Core/project.json | 122 ++- .../Emby.Server.Implementations.csproj | 26 +- .../Emby.Server.Implementations.nuget.targets | 6 - .../FileOrganization/EpisodeFileOrganizer.cs | 3 +- .../SocketSharp/WebSocketSharpListener.cs | 3 +- .../Library/LibraryManager.cs | 13 +- .../Library/Resolvers/Audio/MusicAlbumResolver.cs | 3 +- .../Library/Resolvers/BaseVideoResolver.cs | 6 +- .../Library/Resolvers/Movies/MovieResolver.cs | 6 +- .../Library/Resolvers/TV/SeriesResolver.cs | 3 +- .../LiveTv/Listings/XmlTvListingsProvider.cs | 8 +- .../Logging/PatternsLogger.cs | 63 -- Emby.Server.Implementations/packages.config | 5 +- Emby.Server.Implementations/project.json | 17 - .../MediaBrowser.Server.Mono.csproj | 4 - MediaBrowser.Server.Mono/Program.cs | 27 +- MediaBrowser.Server.Mono/packages.config | 1 - .../Cryptography/CertificateGenerator.cs | 2 +- .../ImageEncoderHelper.cs | 11 +- .../MediaBrowser.Server.Startup.Common.csproj | 20 +- MediaBrowser.Server.Startup.Common/packages.config | 4 +- MediaBrowser.ServerApplication/MainStartup.cs | 24 +- .../MediaBrowser.ServerApplication.csproj | 4 - MediaBrowser.ServerApplication/packages.config | 1 - MediaBrowser.WebDashboard/packages.config | 2 - MediaBrowser.sln | 359 +++++++ ServiceStack/FilterAttributeCache.cs | 27 + ServiceStack/Host/ActionContext.cs | 27 + ServiceStack/Host/ContentTypes.cs | 77 ++ ServiceStack/Host/HttpResponseStreamWrapper.cs | 95 ++ ServiceStack/Host/RestHandler.cs | 200 ++++ ServiceStack/Host/RestPath.cs | 443 ++++++++ ServiceStack/Host/ServiceController.cs | 220 ++++ ServiceStack/Host/ServiceExec.cs | 156 +++ ServiceStack/Host/ServiceMetadata.cs | 27 + ServiceStack/HttpHandlerFactory.cs | 27 + ServiceStack/HttpRequestExtensions.cs | 127 +++ ServiceStack/HttpResponseExtensionsInternal.cs | 237 +++++ ServiceStack/HttpResult.cs | 250 +++++ ServiceStack/HttpUtils.cs | 34 + ServiceStack/Properties/AssemblyInfo.cs | 25 + ServiceStack/ReflectionExtensions.cs | 270 +++++ ServiceStack/ServiceStack.csproj | 131 +++ ServiceStack/ServiceStack.nuget.targets | 6 + ServiceStack/ServiceStack.xproj | 19 + ServiceStack/ServiceStackHost.Runtime.cs | 74 ++ ServiceStack/ServiceStackHost.cs | 104 ++ ServiceStack/StringMapTypeDeserializer.cs | 126 +++ ServiceStack/UrlExtensions.cs | 33 + ServiceStack/packages.config | 3 + ServiceStack/project.json | 17 + SocketHttpListener.Portable/ByteOrder.cs | 17 + SocketHttpListener.Portable/CloseEventArgs.cs | 90 ++ SocketHttpListener.Portable/CloseStatusCode.cs | 94 ++ SocketHttpListener.Portable/CompressionMethod.cs | 23 + SocketHttpListener.Portable/ErrorEventArgs.cs | 46 + SocketHttpListener.Portable/Ext.cs | 1089 ++++++++++++++++++++ SocketHttpListener.Portable/Fin.cs | 8 + SocketHttpListener.Portable/HttpBase.cs | 104 ++ SocketHttpListener.Portable/HttpResponse.cs | 161 +++ SocketHttpListener.Portable/Mask.cs | 8 + SocketHttpListener.Portable/MessageEventArgs.cs | 96 ++ .../Net/AuthenticationSchemeSelector.cs | 6 + SocketHttpListener.Portable/Net/ChunkStream.cs | 371 +++++++ .../Net/ChunkedInputStream.cs | 160 +++ SocketHttpListener.Portable/Net/CookieHelper.cs | 144 +++ .../Net/EndPointListener.cs | 368 +++++++ SocketHttpListener.Portable/Net/EndPointManager.cs | 165 +++ SocketHttpListener.Portable/Net/HttpConnection.cs | 550 ++++++++++ SocketHttpListener.Portable/Net/HttpListener.cs | 299 ++++++ .../Net/HttpListenerBasicIdentity.cs | 70 ++ .../Net/HttpListenerContext.cs | 201 ++++ .../Net/HttpListenerPrefixCollection.cs | 97 ++ .../Net/HttpListenerRequest.cs | 654 ++++++++++++ .../Net/HttpListenerResponse.cs | 517 ++++++++++ SocketHttpListener.Portable/Net/HttpStatusCode.cs | 321 ++++++ .../Net/HttpStreamAsyncResult.cs | 77 ++ SocketHttpListener.Portable/Net/HttpVersion.cs | 16 + SocketHttpListener.Portable/Net/ListenerPrefix.cs | 148 +++ SocketHttpListener.Portable/Net/RequestStream.cs | 231 +++++ SocketHttpListener.Portable/Net/ResponseStream.cs | 316 ++++++ .../Net/WebHeaderCollection.cs | 391 +++++++ .../Net/WebSockets/HttpListenerWebSocketContext.cs | 347 +++++++ .../Net/WebSockets/WebSocketContext.cs | 183 ++++ SocketHttpListener.Portable/Opcode.cs | 43 + SocketHttpListener.Portable/PayloadData.cs | 149 +++ .../Primitives/HttpListenerException.cs | 17 + .../Primitives/ICertificate.cs | 12 + .../Primitives/IStreamFactory.cs | 18 + .../Primitives/ITextEncoding.cs | 17 + .../Properties/AssemblyInfo.cs | 30 + SocketHttpListener.Portable/Rsv.cs | 8 + .../SocketHttpListener.Portable.csproj | 109 ++ .../SocketHttpListener.Portable.nuget.targets | 6 + SocketHttpListener.Portable/WebSocket.cs | 898 ++++++++++++++++ SocketHttpListener.Portable/WebSocketException.cs | 60 ++ SocketHttpListener.Portable/WebSocketFrame.cs | 578 +++++++++++ SocketHttpListener.Portable/WebSocketState.cs | 35 + SocketHttpListener.Portable/packages.config | 5 + SocketHttpListener.Portable/project.json | 17 + 102 files changed, 12649 insertions(+), 233 deletions(-) delete mode 100644 Emby.Server.Implementations/Emby.Server.Implementations.nuget.targets delete mode 100644 Emby.Server.Implementations/Logging/PatternsLogger.cs delete mode 100644 Emby.Server.Implementations/project.json create mode 100644 ServiceStack/FilterAttributeCache.cs create mode 100644 ServiceStack/Host/ActionContext.cs create mode 100644 ServiceStack/Host/ContentTypes.cs create mode 100644 ServiceStack/Host/HttpResponseStreamWrapper.cs create mode 100644 ServiceStack/Host/RestHandler.cs create mode 100644 ServiceStack/Host/RestPath.cs create mode 100644 ServiceStack/Host/ServiceController.cs create mode 100644 ServiceStack/Host/ServiceExec.cs create mode 100644 ServiceStack/Host/ServiceMetadata.cs create mode 100644 ServiceStack/HttpHandlerFactory.cs create mode 100644 ServiceStack/HttpRequestExtensions.cs create mode 100644 ServiceStack/HttpResponseExtensionsInternal.cs create mode 100644 ServiceStack/HttpResult.cs create mode 100644 ServiceStack/HttpUtils.cs create mode 100644 ServiceStack/Properties/AssemblyInfo.cs create mode 100644 ServiceStack/ReflectionExtensions.cs create mode 100644 ServiceStack/ServiceStack.csproj create mode 100644 ServiceStack/ServiceStack.nuget.targets create mode 100644 ServiceStack/ServiceStack.xproj create mode 100644 ServiceStack/ServiceStackHost.Runtime.cs create mode 100644 ServiceStack/ServiceStackHost.cs create mode 100644 ServiceStack/StringMapTypeDeserializer.cs create mode 100644 ServiceStack/UrlExtensions.cs create mode 100644 ServiceStack/packages.config create mode 100644 ServiceStack/project.json create mode 100644 SocketHttpListener.Portable/ByteOrder.cs create mode 100644 SocketHttpListener.Portable/CloseEventArgs.cs create mode 100644 SocketHttpListener.Portable/CloseStatusCode.cs create mode 100644 SocketHttpListener.Portable/CompressionMethod.cs create mode 100644 SocketHttpListener.Portable/ErrorEventArgs.cs create mode 100644 SocketHttpListener.Portable/Ext.cs create mode 100644 SocketHttpListener.Portable/Fin.cs create mode 100644 SocketHttpListener.Portable/HttpBase.cs create mode 100644 SocketHttpListener.Portable/HttpResponse.cs create mode 100644 SocketHttpListener.Portable/Mask.cs create mode 100644 SocketHttpListener.Portable/MessageEventArgs.cs create mode 100644 SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs create mode 100644 SocketHttpListener.Portable/Net/ChunkStream.cs create mode 100644 SocketHttpListener.Portable/Net/ChunkedInputStream.cs create mode 100644 SocketHttpListener.Portable/Net/CookieHelper.cs create mode 100644 SocketHttpListener.Portable/Net/EndPointListener.cs create mode 100644 SocketHttpListener.Portable/Net/EndPointManager.cs create mode 100644 SocketHttpListener.Portable/Net/HttpConnection.cs create mode 100644 SocketHttpListener.Portable/Net/HttpListener.cs create mode 100644 SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs create mode 100644 SocketHttpListener.Portable/Net/HttpListenerContext.cs create mode 100644 SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs create mode 100644 SocketHttpListener.Portable/Net/HttpListenerRequest.cs create mode 100644 SocketHttpListener.Portable/Net/HttpListenerResponse.cs create mode 100644 SocketHttpListener.Portable/Net/HttpStatusCode.cs create mode 100644 SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs create mode 100644 SocketHttpListener.Portable/Net/HttpVersion.cs create mode 100644 SocketHttpListener.Portable/Net/ListenerPrefix.cs create mode 100644 SocketHttpListener.Portable/Net/RequestStream.cs create mode 100644 SocketHttpListener.Portable/Net/ResponseStream.cs create mode 100644 SocketHttpListener.Portable/Net/WebHeaderCollection.cs create mode 100644 SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs create mode 100644 SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs create mode 100644 SocketHttpListener.Portable/Opcode.cs create mode 100644 SocketHttpListener.Portable/PayloadData.cs create mode 100644 SocketHttpListener.Portable/Primitives/HttpListenerException.cs create mode 100644 SocketHttpListener.Portable/Primitives/ICertificate.cs create mode 100644 SocketHttpListener.Portable/Primitives/IStreamFactory.cs create mode 100644 SocketHttpListener.Portable/Primitives/ITextEncoding.cs create mode 100644 SocketHttpListener.Portable/Properties/AssemblyInfo.cs create mode 100644 SocketHttpListener.Portable/Rsv.cs create mode 100644 SocketHttpListener.Portable/SocketHttpListener.Portable.csproj create mode 100644 SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets create mode 100644 SocketHttpListener.Portable/WebSocket.cs create mode 100644 SocketHttpListener.Portable/WebSocketException.cs create mode 100644 SocketHttpListener.Portable/WebSocketFrame.cs create mode 100644 SocketHttpListener.Portable/WebSocketState.cs create mode 100644 SocketHttpListener.Portable/packages.config create mode 100644 SocketHttpListener.Portable/project.json (limited to 'Emby.Server.Implementations/FileOrganization') diff --git a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs index 3410ef003..242898e33 100644 --- a/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs +++ b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs @@ -8,9 +8,6 @@ using MediaBrowser.Model.Logging; using System; using System.IO; using System.Linq; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; namespace Emby.Drawing.ImageMagick @@ -19,17 +16,15 @@ namespace Emby.Drawing.ImageMagick { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; + private readonly Func _httpClientFactory; private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; - public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config) + public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, Func httpClientFactory, IFileSystem fileSystem) { _logger = logger; _appPaths = appPaths; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; - _config = config; LogVersion(); } @@ -260,7 +255,7 @@ namespace Emby.Drawing.ImageMagick { var currentImageSize = new ImageSize(imageWidth, imageHeight); - var task = new PlayedIndicatorDrawer(_appPaths, _httpClient, _fileSystem).DrawPlayedIndicator(wand, currentImageSize); + var task = new PlayedIndicatorDrawer(_appPaths, _httpClientFactory(), _fileSystem).DrawPlayedIndicator(wand, currentImageSize); Task.WaitAll(task); } else if (options.UnplayedCount.HasValue) diff --git a/Emby.Server.Core/Emby.Server.Core.xproj b/Emby.Server.Core/Emby.Server.Core.xproj index fefaa6284..00f7664bd 100644 --- a/Emby.Server.Core/Emby.Server.Core.xproj +++ b/Emby.Server.Core/Emby.Server.Core.xproj @@ -16,6 +16,7 @@ 2.0 + diff --git a/Emby.Server.Core/project.json b/Emby.Server.Core/project.json index 5d14aac3f..93528bb99 100644 --- a/Emby.Server.Core/project.json +++ b/Emby.Server.Core/project.json @@ -2,7 +2,7 @@ "version": "1.0.0-*", "dependencies": { - + }, "frameworks": { @@ -56,65 +56,75 @@ "Emby.Drawing": { "target": "project" }, - "SocketHttpListener.Portable": "1.0.50" - } - }, - "netstandard1.6": { - "imports": "dnxcore50", - "dependencies": { - "NETStandard.Library": "1.6.0", - "System.AppDomain": "2.0.11", - "System.Globalization.Extensions": "4.0.1", - "System.IO.FileSystem.Watcher": "4.0.0", - "System.Net.Security": "4.0.0", - "System.Security.Cryptography.X509Certificates": "4.1.0", - "System.Runtime.Extensions": "4.1.0", - "SocketHttpListener.Portable": "1.0.50", - "MediaBrowser.Model": { - "target": "project" - }, - "MediaBrowser.Common": { - "target": "project" - }, - "MediaBrowser.Controller": { - "target": "project" - }, - "Emby.Common.Implementations": { - "target": "project" - }, - "Mono.Nat": { - "target": "project" - }, - "Emby.Server.Implementations": { - "target": "project" - }, - "MediaBrowser.Server.Implementations": { - "target": "project" - }, - "Emby.Dlna": { - "target": "project" - }, - "Emby.Photos": { - "target": "project" - }, - "MediaBrowser.Api": { - "target": "project" - }, - "MediaBrowser.MediaEncoding": { - "target": "project" - }, - "MediaBrowser.XbmcMetadata": { - "target": "project" - }, - "MediaBrowser.LocalMetadata": { + "ServiceStack": { "target": "project" }, - "MediaBrowser.WebDashboard": { - "target": "project" - }, - "Emby.Drawing": { + "SocketHttpListener.Portable": { "target": "project" } + }, + "netstandard1.6": { + "imports": "dnxcore50", + "dependencies": { + "NETStandard.Library": "1.6.0", + "System.AppDomain": "2.0.11", + "System.Globalization.Extensions": "4.0.1", + "System.IO.FileSystem.Watcher": "4.0.0", + "System.Net.Security": "4.0.0", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "MediaBrowser.Model": { + "target": "project" + }, + "MediaBrowser.Common": { + "target": "project" + }, + "MediaBrowser.Controller": { + "target": "project" + }, + "Emby.Common.Implementations": { + "target": "project" + }, + "Mono.Nat": { + "target": "project" + }, + "Emby.Server.Implementations": { + "target": "project" + }, + "MediaBrowser.Server.Implementations": { + "target": "project" + }, + "Emby.Dlna": { + "target": "project" + }, + "Emby.Photos": { + "target": "project" + }, + "MediaBrowser.Api": { + "target": "project" + }, + "MediaBrowser.MediaEncoding": { + "target": "project" + }, + "MediaBrowser.XbmcMetadata": { + "target": "project" + }, + "MediaBrowser.LocalMetadata": { + "target": "project" + }, + "MediaBrowser.WebDashboard": { + "target": "project" + }, + "Emby.Drawing": { + "target": "project" + }, + "SocketHttpListener.Portable": { + "target": "project" + }, + "ServiceStack": { + "target": "project" + } + } } } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 72053daa0..8f92f062c 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -163,7 +163,6 @@ - @@ -275,23 +274,24 @@ {2e781478-814d-4a48-9d80-bff206441a65} MediaBrowser.Server.Implementations - - ..\ThirdParty\ServiceStack\ServiceStack.dll - - - ..\packages\UniversalDetector.1.0.1\lib\portable-net45+sl4+wp71+win8+wpa81\UniversalDetector.dll - True - + + {680a1709-25eb-4d52-a87f-ee03ffd94baa} + ServiceStack + + + {4f26d5d8-a7b0-42b3-ba42-7cb7d245934e} + SocketHttpListener.Portable + - ..\packages\Emby.XmlTv.1.0.0.63\lib\portable-net45+win8\Emby.XmlTv.dll + ..\packages\Emby.XmlTv.1.0.1\lib\portable-net45+win8\Emby.XmlTv.dll True - - ..\packages\MediaBrowser.Naming.1.0.0.59\lib\portable-net45+win8\MediaBrowser.Naming.dll + + ..\packages\MediaBrowser.Naming.1.0.2\lib\portable-net45+win8\MediaBrowser.Naming.dll True - - ..\packages\Patterns.Logging.1.0.0.6\lib\portable-net45+win8\Patterns.Logging.dll + + ..\packages\UniversalDetector.1.0.1\lib\portable-net45+sl4+wp71+win8+wpa81\UniversalDetector.dll True diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.nuget.targets b/Emby.Server.Implementations/Emby.Server.Implementations.nuget.targets deleted file mode 100644 index e69ce0e64..000000000 --- a/Emby.Server.Implementations/Emby.Server.Implementations.nuget.targets +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index 3f4ede478..cc2dcb6fd 100644 --- a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -15,7 +15,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; -using Emby.Server.Implementations.Logging; using MediaBrowser.Common.IO; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.IO; @@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.FileOrganization } var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - var resolver = new EpisodeResolver(namingOptions, new PatternsLogger()); + var resolver = new EpisodeResolver(namingOptions, new NullLogger()); var episodeInfo = resolver.Resolve(path, false) ?? new MediaBrowser.Naming.TV.EpisodeInfo(); diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index db7b9bc4b..94cc383a7 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Emby.Server.Implementations.Logging; using MediaBrowser.Common.Net; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; @@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp public void Start(IEnumerable urlPrefixes) { if (_listener == null) - _listener = new HttpListener(new PatternsLogger(_logger), _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); + _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); _listener.EnableDualMode = _enableDualMode; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8800777ca..13c6485e5 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -29,7 +29,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library.Resolvers; using Emby.Server.Implementations.Library.Validators; -using Emby.Server.Implementations.Logging; using Emby.Server.Implementations.ScheduledTasks; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Channels; @@ -2266,7 +2265,7 @@ namespace Emby.Server.Implementations.Library public bool IsVideoFile(string path, LibraryOptions libraryOptions) { - var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new PatternsLogger()); + var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new NullLogger()); return resolver.IsVideoFile(path); } @@ -2294,7 +2293,7 @@ namespace Emby.Server.Implementations.Library public bool FillMissingEpisodeNumbersFromPath(Episode episode) { var resolver = new EpisodeResolver(GetNamingOptions(), - new PatternsLogger()); + new NullLogger()); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd || episode.VideoType == VideoType.HdDvd; @@ -2440,7 +2439,7 @@ namespace Emby.Server.Implementations.Library public ItemLookupInfo ParseName(string name) { - var resolver = new VideoResolver(GetNamingOptions(), new PatternsLogger()); + var resolver = new VideoResolver(GetNamingOptions(), new NullLogger()); var result = resolver.CleanDateTime(name); var cleanName = resolver.CleanString(result.Name); @@ -2459,7 +2458,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, false)) .ToList(); - var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger()); + var videoListResolver = new VideoListResolver(GetNamingOptions(), new NullLogger()); var videos = videoListResolver.Resolve(fileSystemChildren); @@ -2505,7 +2504,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, false)) .ToList(); - var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger()); + var videoListResolver = new VideoListResolver(GetNamingOptions(), new NullLogger()); var videos = videoListResolver.Resolve(fileSystemChildren); @@ -2628,7 +2627,7 @@ namespace Emby.Server.Implementations.Library private void SetExtraTypeFromFilename(Video item) { - var resolver = new ExtraResolver(GetNamingOptions(), new PatternsLogger(), new RegexProvider()); + var resolver = new ExtraResolver(GetNamingOptions(), new NullLogger(), new RegexProvider()); var result = resolver.GetExtraInfo(item.Path); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index f8e105195..871b2d46d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -8,7 +8,6 @@ using MediaBrowser.Naming.Audio; using System; using System.Collections.Generic; using System.IO; -using Emby.Server.Implementations.Logging; using MediaBrowser.Common.IO; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Configuration; @@ -164,7 +163,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio { var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(libraryOptions); - var parser = new AlbumParser(namingOptions, new PatternsLogger()); + var parser = new AlbumParser(namingOptions, new NullLogger()); var result = parser.ParseMultiPart(path); return result.IsMultiPart; diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index b7819eb68..d9c3ac5ab 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Naming.Video; using System; using System.IO; -using Emby.Server.Implementations.Logging; +using MediaBrowser.Model.Logging; namespace Emby.Server.Implementations.Library.Resolvers { @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Library.Resolvers var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); // If the path is a file check for a matching extensions - var parser = new MediaBrowser.Naming.Video.VideoResolver(namingOptions, new PatternsLogger()); + var parser = new MediaBrowser.Naming.Video.VideoResolver(namingOptions, new NullLogger()); if (args.IsDirectory) { @@ -258,7 +258,7 @@ namespace Emby.Server.Implementations.Library.Resolvers { var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - var resolver = new Format3DParser(namingOptions, new PatternsLogger()); + var resolver = new Format3DParser(namingOptions, new NullLogger()); var result = resolver.Parse(video.Path); Set3DFormat(video, result.Is3D, result.Format3D); diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index d8c8b2024..002505b56 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -11,10 +11,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Emby.Server.Implementations.Logging; using MediaBrowser.Common.IO; using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; namespace Emby.Server.Implementations.Library.Resolvers.Movies { @@ -133,7 +133,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - var resolver = new VideoListResolver(namingOptions, new PatternsLogger()); + var resolver = new VideoListResolver(namingOptions, new NullLogger()); var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList(); var result = new MultiItemResolverResult @@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies } var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); - var resolver = new StackResolver(namingOptions, new PatternsLogger()); + var resolver = new StackResolver(namingOptions, new NullLogger()); var result = resolver.ResolveDirectories(folderPaths); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 44eb0e3e2..e5cad9f91 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Emby.Server.Implementations.Logging; using MediaBrowser.Common.IO; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Configuration; @@ -171,7 +170,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV .ToList(); } - var episodeResolver = new MediaBrowser.Naming.TV.EpisodeResolver(namingOptions, new PatternsLogger()); + var episodeResolver = new MediaBrowser.Naming.TV.EpisodeResolver(namingOptions, new NullLogger()); var episodeInfo = episodeResolver.Resolve(fullName, false, false); if (episodeInfo != null && episodeInfo.EpisodeNumber.HasValue) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 57723e3c5..33db69ee2 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); - var reader = new XmlTvReader(path, GetLanguage(), null); + var reader = new XmlTvReader(path, GetLanguage()); var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken); return results.Select(p => GetProgramInfo(p, info)); @@ -175,7 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { // Add the channel image url var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); - var reader = new XmlTvReader(path, GetLanguage(), null); + var reader = new XmlTvReader(path, GetLanguage()); var results = reader.GetChannels().ToList(); if (channels != null) @@ -208,7 +208,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { // In theory this should never be called because there is always only one lineup var path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false); - var reader = new XmlTvReader(path, GetLanguage(), null); + var reader = new XmlTvReader(path, GetLanguage()); var results = reader.GetChannels(); // Should this method be async? @@ -219,7 +219,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { // In theory this should never be called because there is always only one lineup var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); - var reader = new XmlTvReader(path, GetLanguage(), null); + var reader = new XmlTvReader(path, GetLanguage()); var results = reader.GetChannels(); // Should this method be async? diff --git a/Emby.Server.Implementations/Logging/PatternsLogger.cs b/Emby.Server.Implementations/Logging/PatternsLogger.cs deleted file mode 100644 index 6dbf33d61..000000000 --- a/Emby.Server.Implementations/Logging/PatternsLogger.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Patterns.Logging; -using System; - -namespace Emby.Server.Implementations.Logging -{ - public class PatternsLogger : ILogger - { - private readonly MediaBrowser.Model.Logging.ILogger _logger; - - public PatternsLogger() - : this(new MediaBrowser.Model.Logging.NullLogger()) - { - } - - public PatternsLogger(MediaBrowser.Model.Logging.ILogger logger) - { - _logger = logger; - } - - public void Debug(string message, params object[] paramList) - { - _logger.Debug(message, paramList); - } - - public void Error(string message, params object[] paramList) - { - _logger.Error(message, paramList); - } - - public void ErrorException(string message, Exception exception, params object[] paramList) - { - _logger.ErrorException(message, exception, paramList); - } - - public void Fatal(string message, params object[] paramList) - { - _logger.Fatal(message, paramList); - } - - public void FatalException(string message, Exception exception, params object[] paramList) - { - _logger.FatalException(message, exception, paramList); - } - - public void Info(string message, params object[] paramList) - { - _logger.Info(message, paramList); - } - - public void Warn(string message, params object[] paramList) - { - _logger.Warn(message, paramList); - } - - public void Log(LogSeverity severity, string message, params object[] paramList) - { - } - - public void LogMultiline(string message, LogSeverity severity, System.Text.StringBuilder additionalContent) - { - } - } -} diff --git a/Emby.Server.Implementations/packages.config b/Emby.Server.Implementations/packages.config index 75c14aa58..71fdbffc2 100644 --- a/Emby.Server.Implementations/packages.config +++ b/Emby.Server.Implementations/packages.config @@ -1,7 +1,6 @@  - - + + - \ No newline at end of file diff --git a/Emby.Server.Implementations/project.json b/Emby.Server.Implementations/project.json deleted file mode 100644 index 13bdedb41..000000000 --- a/Emby.Server.Implementations/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "frameworks":{ - "netstandard1.6":{ - "dependencies":{ - "NETStandard.Library":"1.6.0", - } - }, - ".NETPortable,Version=v4.5,Profile=Profile7":{ - "buildOptions": { - "define": [ ] - }, - "frameworkAssemblies":{ - - } - } - } -} \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index b62d99a13..b4c89826f 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -72,10 +72,6 @@ ..\packages\NLog.4.4.0-betaV15\lib\net45\NLog.dll True - - ..\packages\Patterns.Logging.1.0.0.6\lib\portable-net45+win8\Patterns.Logging.dll - True - ..\ThirdParty\MediaBrowser.IsoMounting.Linux\MediaBrowser.IsoMounting.Linux.dll diff --git a/MediaBrowser.Server.Mono/Program.cs b/MediaBrowser.Server.Mono/Program.cs index 7c77f67ed..9a8ac7763 100644 --- a/MediaBrowser.Server.Mono/Program.cs +++ b/MediaBrowser.Server.Mono/Program.cs @@ -10,18 +10,21 @@ using System.Linq; using System.Net; using System.Net.Security; using System.Reflection; -using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using System.Threading.Tasks; using Emby.Common.Implementations.EnvironmentInfo; using Emby.Common.Implementations.IO; using Emby.Common.Implementations.Logging; +using Emby.Common.Implementations.Networking; +using Emby.Common.Implementations.Security; using Emby.Server.Core; using Emby.Server.Implementations.IO; using MediaBrowser.Model.System; +using MediaBrowser.Server.Startup.Common.IO; using Mono.Unix.Native; using NLog; using ILogger = MediaBrowser.Model.Logging.ILogger; +using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate; namespace MediaBrowser.Server.Mono { @@ -90,7 +93,22 @@ namespace MediaBrowser.Server.Mono var nativeApp = new MonoApp(options, logManager.GetLogger("App"), environmentInfo); - _appHost = new ApplicationHost(appPaths, logManager, options, fileSystem, nativeApp, new PowerManagement(), "emby.mono.zip", environmentInfo); + var imageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => _appHost.HttpClient, appPaths); + + _appHost = new ApplicationHost(appPaths, + logManager, + options, + fileSystem, + nativeApp, + new PowerManagement(), + "emby.mono.zip", + environmentInfo, + imageEncoder, + new Startup.Common.SystemEvents(logManager.GetLogger("SystemEvents")), + new MemoryStreamProvider(), + new NetworkManager(logManager.GetLogger("NetworkManager")), + GenerateCertificate, + () => Environment.UserDomainName); if (options.ContainsOption("-v")) { @@ -115,6 +133,11 @@ namespace MediaBrowser.Server.Mono Task.WaitAll(task); } + private static void GenerateCertificate(string certPath, string certHost) + { + CertificateGenerator.CreateSelfSignCertificatePfx(certPath, certHost, _logger); + } + private static MonoEnvironmentInfo GetEnvironmentInfo() { var info = new MonoEnvironmentInfo(); diff --git a/MediaBrowser.Server.Mono/packages.config b/MediaBrowser.Server.Mono/packages.config index 8d1ebc141..259ad4182 100644 --- a/MediaBrowser.Server.Mono/packages.config +++ b/MediaBrowser.Server.Mono/packages.config @@ -2,5 +2,4 @@ - \ No newline at end of file diff --git a/MediaBrowser.Server.Startup.Common/Cryptography/CertificateGenerator.cs b/MediaBrowser.Server.Startup.Common/Cryptography/CertificateGenerator.cs index f5a337505..9e14b7713 100644 --- a/MediaBrowser.Server.Startup.Common/Cryptography/CertificateGenerator.cs +++ b/MediaBrowser.Server.Startup.Common/Cryptography/CertificateGenerator.cs @@ -9,7 +9,7 @@ namespace Emby.Common.Implementations.Security { private const string MonoTestRootAgency = "v/4nALBxCE+9JgEC0LnDUvKh6e96PwTpN4Rj+vWnqKT7IAp1iK/JjuqvAg6DQ2vTfv0dTlqffmHH51OyioprcT5nzxcSTsZb/9jcHScG0s3/FRIWnXeLk/fgm7mSYhjUaHNI0m1/NTTktipicjKxo71hGIg9qucCWnDum+Krh/k=AQAB

9jbKxMXEruW2CfZrzhxtull4O8P47+mNsEL+9gf9QsRO1jJ77C+jmzfU6zbzjf8+ViK+q62tCMdC1ZzulwdpXQ==

x5+p198l1PkK0Ga2mRh0SIYSykENpY2aLXoyZD/iUpKYAvATm0/wvKNrE4dKJyPCA+y3hfTdgVag+SP9avvDTQ==ISSjCvXsUfbOGG05eddN1gXxL2pj+jegQRfjpk7RAsnWKvNExzhqd5x+ZuNQyc6QH5wxun54inP4RTUI0P/IaQ==R815VQmR3RIbPqzDXzv5j6CSH6fYlcTiQRtkBsUnzhWmkd/y3XmamO+a8zJFjOCCx9CcjpVuGziivBqi65lVPQ==iYiu0KwMWI/dyqN3RJYUzuuLj02/oTD1pYpwo2rvNCXU1Q5VscOeu2DpNg1gWqI+1RrRCsEoaTNzXB1xtKNlSw==nIfh1LYF8fjRBgMdAH/zt9UKHWiaCnc+jXzq5tkR8HVSKTVdzitD8bl1JgAfFQD8VjSXiCJqluexy/B5SGrCXQ49c78NIQj0hD+J13Y8/E0fUbW1QYbhj6Ff7oHyhaYe1WOQfkp2t/h+llHOdt1HRf7bt7dUknYp7m8bQKGxoYE=
"; - internal static void CreateSelfSignCertificatePfx( + public static void CreateSelfSignCertificatePfx( string fileName, string hostname, ILogger logger) diff --git a/MediaBrowser.Server.Startup.Common/ImageEncoderHelper.cs b/MediaBrowser.Server.Startup.Common/ImageEncoderHelper.cs index e66c000d9..0a1470f93 100644 --- a/MediaBrowser.Server.Startup.Common/ImageEncoderHelper.cs +++ b/MediaBrowser.Server.Startup.Common/ImageEncoderHelper.cs @@ -1,9 +1,10 @@ -using Emby.Drawing; +using System; +using Emby.Drawing; using Emby.Drawing.Net; using Emby.Drawing.ImageMagick; using Emby.Server.Core; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; @@ -16,14 +17,14 @@ namespace MediaBrowser.Server.Startup.Common ILogManager logManager, IFileSystem fileSystem, StartupOptions startupOptions, - IHttpClient httpClient, - IServerConfigurationManager config) + Func httpClient, + IApplicationPaths appPaths) { if (!startupOptions.ContainsOption("-enablegdi")) { try { - return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), config.ApplicationPaths, httpClient, fileSystem, config); + return new ImageMagickEncoder(logManager.GetLogger("ImageMagick"), appPaths, httpClient, fileSystem); } catch { diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 262b0e2e8..95da7feab 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -44,8 +44,8 @@ ..\packages\ini-parser.2.3.0\lib\net20\INIFileParser.dll True
- - ..\packages\MediaBrowser.Naming.1.0.0.59\lib\portable-net45+win8\MediaBrowser.Naming.dll + + ..\packages\MediaBrowser.Naming.1.0.2\lib\portable-net45+win8\MediaBrowser.Naming.dll True @@ -60,14 +60,6 @@ False ..\packages\Mono.Posix.4.0.0.0\lib\net40\Mono.Posix.dll - - ..\packages\Patterns.Logging.1.0.0.7\lib\netstandard1.3\Patterns.Logging.dll - True - - - False - ..\ThirdParty\ServiceStack\ServiceStack.dll - ..\packages\ServiceStack.Text.4.5.4\lib\net45\ServiceStack.Text.dll True @@ -76,10 +68,6 @@ ..\packages\SimpleInjector.3.2.4\lib\net45\SimpleInjector.dll True - - ..\packages\SocketHttpListener.Portable.1.0.50\lib\portable-net45+win8\SocketHttpListener.dll - True - @@ -212,6 +200,10 @@ {21002819-c39a-4d3e-be83-2a276a77fb1f} RSSDP + + {680a1709-25eb-4d52-a87f-ee03ffd94baa} + ServiceStack + diff --git a/MediaBrowser.Server.Startup.Common/packages.config b/MediaBrowser.Server.Startup.Common/packages.config index b364fe32b..db6116c82 100644 --- a/MediaBrowser.Server.Startup.Common/packages.config +++ b/MediaBrowser.Server.Startup.Common/packages.config @@ -1,11 +1,9 @@  - + - - \ No newline at end of file diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index adfc0a588..09c948a4a 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -20,11 +20,14 @@ using System.Windows.Forms; using Emby.Common.Implementations.EnvironmentInfo; using Emby.Common.Implementations.IO; using Emby.Common.Implementations.Logging; +using Emby.Common.Implementations.Networking; +using Emby.Common.Implementations.Security; using Emby.Server.Core; using Emby.Server.Core.Browser; using Emby.Server.Implementations.IO; using ImageMagickSharp; using MediaBrowser.Common.Net; +using MediaBrowser.Server.Startup.Common.IO; namespace MediaBrowser.ServerApplication { @@ -64,8 +67,8 @@ namespace MediaBrowser.ServerApplication return false; } /// - /// Defines the entry point of the application. - /// + /// Defines the entry point of the application. + /// public static void Main() { var options = new StartupOptions(); @@ -319,14 +322,22 @@ namespace MediaBrowser.ServerApplication IsRunningAsService = runService }; + var imageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => _appHost.HttpClient, appPaths); + _appHost = new ApplicationHost(appPaths, logManager, options, fileSystem, - nativeApp, + nativeApp, new PowerManagement(), "emby.windows.zip", - new EnvironmentInfo()); + new EnvironmentInfo(), + imageEncoder, + new Server.Startup.Common.SystemEvents(logManager.GetLogger("SystemEvents")), + new RecyclableMemoryStreamProvider(), + new NetworkManager(logManager.GetLogger("NetworkManager")), + GenerateCertificate, + () => Environment.UserDomainName); var initProgress = new Progress(); @@ -367,6 +378,11 @@ namespace MediaBrowser.ServerApplication } } + private static void GenerateCertificate(string certPath, string certHost) + { + CertificateGenerator.CreateSelfSignCertificatePfx(certPath, certHost, _logger); + } + private static ServerNotifyIcon _serverNotifyIcon; private static TaskScheduler _mainTaskScheduler; private static void ShowTrayIcon() diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index b082b14d8..4a4a3ca43 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -78,10 +78,6 @@ ..\packages\NLog.4.4.0-betaV15\lib\net45\NLog.dll True - - ..\packages\Patterns.Logging.1.0.0.6\lib\portable-net45+win8\Patterns.Logging.dll - True - diff --git a/MediaBrowser.ServerApplication/packages.config b/MediaBrowser.ServerApplication/packages.config index 8dbcaa2b9..169cf7c4b 100644 --- a/MediaBrowser.ServerApplication/packages.config +++ b/MediaBrowser.ServerApplication/packages.config @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/MediaBrowser.WebDashboard/packages.config b/MediaBrowser.WebDashboard/packages.config index 0a650254e..6b8deb9c9 100644 --- a/MediaBrowser.WebDashboard/packages.config +++ b/MediaBrowser.WebDashboard/packages.config @@ -1,5 +1,3 @@  - - \ No newline at end of file diff --git a/MediaBrowser.sln b/MediaBrowser.sln index aff9c5c11..292d0345c 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -78,6 +78,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.Drawing.ImageMagick", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Emby.Drawing.Net", "Emby.Drawing.Net\Emby.Drawing.Net.csproj", "{C97A239E-A96C-4D64-A844-CCF8CC30AECB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceStack", "ServiceStack\ServiceStack.csproj", "{680A1709-25EB-4D52-A87F-EE03FFD94BAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SocketHttpListener.Portable", "SocketHttpListener.Portable\SocketHttpListener.Portable.csproj", "{4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +99,11 @@ Global Release|Win32 = Release|Win32 Release|x64 = Release|x64 Release|x86 = Release|x86 + Signed|Any CPU = Signed|Any CPU + Signed|Mixed Platforms = Signed|Mixed Platforms + Signed|Win32 = Signed|Win32 + Signed|x64 = Signed|x64 + Signed|x86 = Signed|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -120,6 +129,16 @@ Global {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x64.ActiveCfg = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.ActiveCfg = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|x86.Build.0 = Release|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|Win32.Build.0 = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|x64.Build.0 = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Signed|x86.Build.0 = Release Mono|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -143,6 +162,16 @@ Global {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x64.ActiveCfg = Release|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x86.ActiveCfg = Release|Any CPU {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Release|x86.Build.0 = Release|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|Win32.Build.0 = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|x64.Build.0 = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {4FD51AC5-2C16-4308-A993-C3A84F3B4582}.Signed|x86.Build.0 = Release Mono|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -166,6 +195,16 @@ Global {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x64.ActiveCfg = Release|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.ActiveCfg = Release|Any CPU {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Release|x86.Build.0 = Release|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|Win32.Build.0 = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|x64.Build.0 = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {9142EEFA-7570-41E1-BFCC-468BB571AF2F}.Signed|x86.Build.0 = Release Mono|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -192,6 +231,16 @@ Global {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x64.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.ActiveCfg = Release|Any CPU {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release|x86.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|Any CPU.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|Win32.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|Win32.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|x64.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|x64.Build.0 = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|x86.ActiveCfg = Release|Any CPU + {7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Signed|x86.Build.0 = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Any CPU.Build.0 = Debug|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -215,6 +264,16 @@ Global {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x64.ActiveCfg = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.ActiveCfg = Release|Any CPU {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.Build.0 = Release|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|Win32.Build.0 = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|x64.Build.0 = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {5624B7B5-B5A7-41D8-9F10-CC5611109619}.Signed|x86.Build.0 = Release Mono|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -236,6 +295,16 @@ Global {2E781478-814D-4A48-9D80-BFF206441A65}.Release|Win32.ActiveCfg = Release|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Release|x64.ActiveCfg = Release|Any CPU {2E781478-814D-4A48-9D80-BFF206441A65}.Release|x86.ActiveCfg = Release|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|Win32.Build.0 = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|x64.Build.0 = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {2E781478-814D-4A48-9D80-BFF206441A65}.Signed|x86.Build.0 = Release Mono|Any CPU {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Debug|Any CPU.Build.0 = Debug|Any CPU {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -257,6 +326,16 @@ Global {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Release|Win32.ActiveCfg = Release|Any CPU {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Release|x64.ActiveCfg = Release|Any CPU {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Release|x86.ActiveCfg = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|Any CPU.Build.0 = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|Win32.ActiveCfg = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|Win32.Build.0 = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|x64.ActiveCfg = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|x64.Build.0 = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|x86.ActiveCfg = Release|Any CPU + {E22BFD35-0FCD-4A85-978A-C22DCD73A081}.Signed|x86.Build.0 = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -278,6 +357,16 @@ Global {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|Win32.ActiveCfg = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|x64.ActiveCfg = Release|Any CPU {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Release|x86.ActiveCfg = Release|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|Win32.Build.0 = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|x64.Build.0 = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {442B5058-DCAF-4263-BB6A-F21E31120A1B}.Signed|x86.Build.0 = Release Mono|Any CPU {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Debug|Any CPU.Build.0 = Debug|Any CPU {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -300,6 +389,16 @@ Global {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Release|Win32.ActiveCfg = Release|Any CPU {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Release|x64.ActiveCfg = Release|Any CPU {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Release|x86.ActiveCfg = Release|Any CPU + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|Any CPU.Build.0 = Release|Any CPU + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|Mixed Platforms.ActiveCfg = Debug|x86 + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|Mixed Platforms.Build.0 = Debug|x86 + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|Win32.ActiveCfg = Debug|x86 + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|Win32.Build.0 = Debug|x86 + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|x64.ActiveCfg = Release|Any CPU + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|x64.Build.0 = Release|Any CPU + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|x86.ActiveCfg = Debug|x86 + {94ADE4D3-B7EC-45CD-A200-CC469433072B}.Signed|x86.Build.0 = Debug|x86 {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -321,6 +420,16 @@ Global {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|Win32.ActiveCfg = Release|Any CPU {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|x64.ActiveCfg = Release|Any CPU {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|x86.ActiveCfg = Release|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|Win32.Build.0 = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|x64.Build.0 = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Signed|x86.Build.0 = Release Mono|Any CPU {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -342,6 +451,16 @@ Global {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Win32.ActiveCfg = Release|Any CPU {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|x64.ActiveCfg = Release|Any CPU {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|x86.ActiveCfg = Release|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|Any CPU.ActiveCfg = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|Any CPU.Build.0 = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|Mixed Platforms.ActiveCfg = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|Mixed Platforms.Build.0 = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|Win32.ActiveCfg = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|Win32.Build.0 = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|x64.ActiveCfg = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|x64.Build.0 = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|x86.ActiveCfg = Release Mono|Any CPU + {4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Signed|x86.Build.0 = Release Mono|Any CPU {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.Build.0 = Debug|Any CPU {23499896-B135-4527-8574-C26E926EA99E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -363,6 +482,16 @@ Global {23499896-B135-4527-8574-C26E926EA99E}.Release|Win32.ActiveCfg = Release|Any CPU {23499896-B135-4527-8574-C26E926EA99E}.Release|x64.ActiveCfg = Release|Any CPU {23499896-B135-4527-8574-C26E926EA99E}.Release|x86.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|Any CPU.Build.0 = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|Win32.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|Win32.Build.0 = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|x64.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|x64.Build.0 = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|x86.ActiveCfg = Release|Any CPU + {23499896-B135-4527-8574-C26E926EA99E}.Signed|x86.Build.0 = Release|Any CPU {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.Build.0 = Debug|Any CPU {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -384,6 +513,16 @@ Global {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Win32.ActiveCfg = Release|Any CPU {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|x64.ActiveCfg = Release|Any CPU {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|x86.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|Any CPU.Build.0 = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|Win32.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|Win32.Build.0 = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|x64.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|x64.Build.0 = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|x86.ActiveCfg = Release|Any CPU + {7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Signed|x86.Build.0 = Release|Any CPU {175A9388-F352-4586-A6B4-070DED62B644}.Debug|Any CPU.ActiveCfg = Debug|x86 {175A9388-F352-4586-A6B4-070DED62B644}.Debug|Any CPU.Build.0 = Debug|x86 {175A9388-F352-4586-A6B4-070DED62B644}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 @@ -410,6 +549,16 @@ Global {175A9388-F352-4586-A6B4-070DED62B644}.Release|x64.ActiveCfg = Release|Any CPU {175A9388-F352-4586-A6B4-070DED62B644}.Release|x86.ActiveCfg = Release|x86 {175A9388-F352-4586-A6B4-070DED62B644}.Release|x86.Build.0 = Release|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|Any CPU.Build.0 = Release|Any CPU + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|Mixed Platforms.ActiveCfg = Release|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|Mixed Platforms.Build.0 = Release|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|Win32.ActiveCfg = Release|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|Win32.Build.0 = Release|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|x64.ActiveCfg = Release|Any CPU + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|x64.Build.0 = Release|Any CPU + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|x86.ActiveCfg = Release|x86 + {175A9388-F352-4586-A6B4-070DED62B644}.Signed|x86.Build.0 = Release|x86 {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Debug|Any CPU.Build.0 = Debug|Any CPU {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -431,6 +580,16 @@ Global {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Release|Win32.ActiveCfg = Release|Any CPU {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Release|x64.ActiveCfg = Release|Any CPU {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Release|x86.ActiveCfg = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|Any CPU.Build.0 = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|Win32.ActiveCfg = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|Win32.Build.0 = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|x64.ActiveCfg = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|x64.Build.0 = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|x86.ActiveCfg = Release|Any CPU + {B90AB8F2-1BFF-4568-A3FD-2A338A435A75}.Signed|x86.Build.0 = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Any CPU.Build.0 = Debug|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -452,6 +611,16 @@ Global {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|Win32.ActiveCfg = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|x64.ActiveCfg = Release|Any CPU {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Release|x86.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|Any CPU.Build.0 = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|Win32.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|Win32.Build.0 = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|x64.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|x64.Build.0 = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|x86.ActiveCfg = Release|Any CPU + {08FFF49B-F175-4807-A2B5-73B0EBD9F716}.Signed|x86.Build.0 = Release|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -482,6 +651,16 @@ Global {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|x64.Build.0 = Release|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|x86.ActiveCfg = Release|Any CPU {89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|x86.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|Any CPU.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|Win32.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|Win32.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|x64.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|x64.Build.0 = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|x86.ActiveCfg = Release|Any CPU + {89AB4548-770D-41FD-A891-8DAFF44F452C}.Signed|x86.Build.0 = Release|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -512,6 +691,16 @@ Global {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|x64.Build.0 = Release|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|x86.ActiveCfg = Release|Any CPU {713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|x86.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|Any CPU.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|Win32.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|Win32.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|x64.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|x64.Build.0 = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|x86.ActiveCfg = Release|Any CPU + {713F42B5-878E-499D-A878-E4C652B1D5E8}.Signed|x86.Build.0 = Release|Any CPU {5A27010A-09C6-4E86-93EA-437484C10917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A27010A-09C6-4E86-93EA-437484C10917}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A27010A-09C6-4E86-93EA-437484C10917}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -542,6 +731,16 @@ Global {5A27010A-09C6-4E86-93EA-437484C10917}.Release|x64.Build.0 = Release|Any CPU {5A27010A-09C6-4E86-93EA-437484C10917}.Release|x86.ActiveCfg = Release|Any CPU {5A27010A-09C6-4E86-93EA-437484C10917}.Release|x86.Build.0 = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|Any CPU.Build.0 = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|Win32.ActiveCfg = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|Win32.Build.0 = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|x64.ActiveCfg = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|x64.Build.0 = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|x86.ActiveCfg = Release|Any CPU + {5A27010A-09C6-4E86-93EA-437484C10917}.Signed|x86.Build.0 = Release|Any CPU {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Any CPU.Build.0 = Debug|Any CPU {88AE38DF-19D7-406F-A6A9-09527719A21E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -572,6 +771,16 @@ Global {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|x64.Build.0 = Release|Any CPU {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|x86.ActiveCfg = Release|Any CPU {88AE38DF-19D7-406F-A6A9-09527719A21E}.Release|x86.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|Any CPU.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|Win32.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|Win32.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|x64.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|x64.Build.0 = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|x86.ActiveCfg = Release|Any CPU + {88AE38DF-19D7-406F-A6A9-09527719A21E}.Signed|x86.Build.0 = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -602,6 +811,16 @@ Global {E383961B-9356-4D5D-8233-9A1079D03055}.Release|x64.Build.0 = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Release|x86.ActiveCfg = Release|Any CPU {E383961B-9356-4D5D-8233-9A1079D03055}.Release|x86.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|Any CPU.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|Win32.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|Win32.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|x64.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|x64.Build.0 = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|x86.ActiveCfg = Release|Any CPU + {E383961B-9356-4D5D-8233-9A1079D03055}.Signed|x86.Build.0 = Release|Any CPU {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Debug|Any CPU.Build.0 = Debug|Any CPU {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -632,6 +851,16 @@ Global {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Release|x64.Build.0 = Release|Any CPU {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Release|x86.ActiveCfg = Release|Any CPU {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Release|x86.Build.0 = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|Any CPU.Build.0 = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|Win32.ActiveCfg = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|Win32.Build.0 = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|x64.ActiveCfg = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|x64.Build.0 = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|x86.ActiveCfg = Release|Any CPU + {4ACAB6A2-AC9A-4B50-BAEC-1FE4A1F3B8BC}.Signed|x86.Build.0 = Release|Any CPU {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -662,6 +891,16 @@ Global {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|x64.Build.0 = Release|Any CPU {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|x86.ActiveCfg = Release|Any CPU {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|x86.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|Any CPU.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|Win32.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|Win32.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|x64.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|x64.Build.0 = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|x86.ActiveCfg = Release|Any CPU + {21002819-C39A-4D3E-BE83-2A276A77FB1F}.Signed|x86.Build.0 = Release|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -692,6 +931,16 @@ Global {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|x64.Build.0 = Release|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|x86.ActiveCfg = Release|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|x86.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|Any CPU.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|Win32.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|Win32.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|x64.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|x64.Build.0 = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|x86.ActiveCfg = Release|Any CPU + {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Signed|x86.Build.0 = Release|Any CPU {65AA7D67-8059-40CD-91F1-16D02687226C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {65AA7D67-8059-40CD-91F1-16D02687226C}.Debug|Any CPU.Build.0 = Debug|Any CPU {65AA7D67-8059-40CD-91F1-16D02687226C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -722,6 +971,16 @@ Global {65AA7D67-8059-40CD-91F1-16D02687226C}.Release|x64.Build.0 = Release|Any CPU {65AA7D67-8059-40CD-91F1-16D02687226C}.Release|x86.ActiveCfg = Release|Any CPU {65AA7D67-8059-40CD-91F1-16D02687226C}.Release|x86.Build.0 = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|Any CPU.Build.0 = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|Win32.ActiveCfg = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|Win32.Build.0 = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|x64.ActiveCfg = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|x64.Build.0 = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|x86.ActiveCfg = Release|Any CPU + {65AA7D67-8059-40CD-91F1-16D02687226C}.Signed|x86.Build.0 = Release|Any CPU {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -752,6 +1011,16 @@ Global {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Release|x64.Build.0 = Release|Any CPU {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Release|x86.ActiveCfg = Release|Any CPU {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Release|x86.Build.0 = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|Any CPU.Build.0 = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|Win32.ActiveCfg = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|Win32.Build.0 = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|x64.ActiveCfg = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|x64.Build.0 = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|x86.ActiveCfg = Release|Any CPU + {6CFEE013-6E7C-432B-AC37-CABF0880C69A}.Signed|x86.Build.0 = Release|Any CPU {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Debug|Any CPU.Build.0 = Debug|Any CPU {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -782,6 +1051,96 @@ Global {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Release|x64.Build.0 = Release|Any CPU {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Release|x86.ActiveCfg = Release|Any CPU {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Release|x86.Build.0 = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|Any CPU.Build.0 = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|Win32.ActiveCfg = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|Win32.Build.0 = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|x64.ActiveCfg = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|x64.Build.0 = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|x86.ActiveCfg = Release|Any CPU + {C97A239E-A96C-4D64-A844-CCF8CC30AECB}.Signed|x86.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|Win32.ActiveCfg = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|Win32.Build.0 = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|x64.ActiveCfg = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|x64.Build.0 = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|x86.ActiveCfg = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Debug|x86.Build.0 = Debug|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|Mixed Platforms.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|Mixed Platforms.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|Win32.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|Win32.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|x64.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|x64.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|x86.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release Mono|x86.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|Any CPU.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|Win32.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|Win32.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|x64.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|x64.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|x86.ActiveCfg = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Release|x86.Build.0 = Release|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|Any CPU.ActiveCfg = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|Any CPU.Build.0 = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|Mixed Platforms.ActiveCfg = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|Mixed Platforms.Build.0 = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|Win32.ActiveCfg = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|Win32.Build.0 = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|x64.ActiveCfg = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|x64.Build.0 = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|x86.ActiveCfg = Signed|Any CPU + {680A1709-25EB-4D52-A87F-EE03FFD94BAA}.Signed|x86.Build.0 = Signed|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|Win32.ActiveCfg = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|Win32.Build.0 = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|x64.Build.0 = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Debug|x86.Build.0 = Debug|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|Any CPU.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|Mixed Platforms.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|Mixed Platforms.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|Win32.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|Win32.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|x64.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|x64.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|x86.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release Mono|x86.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|Any CPU.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|Win32.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|Win32.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|x64.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|x64.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|x86.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Release|x86.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|Any CPU.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|Any CPU.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|Mixed Platforms.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|Mixed Platforms.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|Win32.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|Win32.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|x64.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|x64.Build.0 = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|x86.ActiveCfg = Release|Any CPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E}.Signed|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ServiceStack/FilterAttributeCache.cs b/ServiceStack/FilterAttributeCache.cs new file mode 100644 index 000000000..378433add --- /dev/null +++ b/ServiceStack/FilterAttributeCache.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using ServiceStack; + +namespace ServiceStack.Support.WebHost +{ + public static class FilterAttributeCache + { + public static MediaBrowser.Model.Services.IHasRequestFilter[] GetRequestFilterAttributes(Type requestDtoType) + { + var attributes = requestDtoType.AllAttributes().OfType().ToList(); + + var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestDtoType); + if (serviceType != null) + { + attributes.AddRange(serviceType.AllAttributes().OfType()); + } + + attributes.Sort((x,y) => x.Priority - y.Priority); + + return attributes.ToArray(); + } + } +} diff --git a/ServiceStack/Host/ActionContext.cs b/ServiceStack/Host/ActionContext.cs new file mode 100644 index 000000000..9f165cff1 --- /dev/null +++ b/ServiceStack/Host/ActionContext.cs @@ -0,0 +1,27 @@ +using System; + +namespace ServiceStack.Host +{ + /// + /// Context to capture IService action + /// + public class ActionContext + { + public const string AnyAction = "ANY"; + + public string Id { get; set; } + + public ActionInvokerFn ServiceAction { get; set; } + public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; } + + public static string Key(Type serviceType, string method, string requestDtoName) + { + return serviceType.FullName + " " + method.ToUpper() + " " + requestDtoName; + } + + public static string AnyKey(Type serviceType, string requestDtoName) + { + return Key(serviceType, AnyAction, requestDtoName); + } + } +} \ No newline at end of file diff --git a/ServiceStack/Host/ContentTypes.cs b/ServiceStack/Host/ContentTypes.cs new file mode 100644 index 000000000..22fdc3e50 --- /dev/null +++ b/ServiceStack/Host/ContentTypes.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public class ContentTypes + { + public static ContentTypes Instance = new ContentTypes(); + + public void SerializeToStream(IRequest req, object response, Stream responseStream) + { + var contentType = req.ResponseContentType; + var serializer = GetResponseSerializer(contentType); + if (serializer == null) + throw new NotSupportedException("ContentType not supported: " + contentType); + + var httpRes = new HttpResponseStreamWrapper(responseStream, req) + { + Dto = req.Response.Dto + }; + serializer(req, response, httpRes); + } + + public Action GetResponseSerializer(string contentType) + { + var serializer = GetStreamSerializer(contentType); + if (serializer == null) return null; + + return (httpReq, dto, httpRes) => serializer(httpReq, dto, httpRes.OutputStream); + } + + public Action GetStreamSerializer(string contentType) + { + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return (r, o, s) => ServiceStackHost.Instance.SerializeToXml(o, s); + + case "application/json": + case "text/json": + return (r, o, s) => ServiceStackHost.Instance.SerializeToJson(o, s); + } + + return null; + } + + public Func GetStreamDeserializer(string contentType) + { + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return ServiceStackHost.Instance.DeserializeXml; + + case "application/json": + case "text/json": + return ServiceStackHost.Instance.DeserializeJson; + } + + return null; + } + + private static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + } +} \ No newline at end of file diff --git a/ServiceStack/Host/HttpResponseStreamWrapper.cs b/ServiceStack/Host/HttpResponseStreamWrapper.cs new file mode 100644 index 000000000..33038da72 --- /dev/null +++ b/ServiceStack/Host/HttpResponseStreamWrapper.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public class HttpResponseStreamWrapper : IHttpResponse + { + private static readonly UTF8Encoding UTF8EncodingWithoutBom = new UTF8Encoding(false); + + public HttpResponseStreamWrapper(Stream stream, IRequest request) + { + this.OutputStream = stream; + this.Request = request; + this.Headers = new Dictionary(); + this.Items = new Dictionary(); + } + + public Dictionary Headers { get; set; } + + public object OriginalResponse + { + get { return null; } + } + + public IRequest Request { get; private set; } + + public int StatusCode { set; get; } + public string StatusDescription { set; get; } + public string ContentType { get; set; } + + public void AddHeader(string name, string value) + { + this.Headers[name] = value; + } + + public string GetHeader(string name) + { + return this.Headers[name]; + } + + public void Redirect(string url) + { + this.Headers["Location"] = url; + } + + public Stream OutputStream { get; private set; } + + public object Dto { get; set; } + + public void Write(string text) + { + var bytes = UTF8EncodingWithoutBom.GetBytes(text); + OutputStream.Write(bytes, 0, bytes.Length); + } + + public bool UseBufferedStream { get; set; } + + public void Close() + { + if (IsClosed) return; + + OutputStream.Dispose(); + IsClosed = true; + } + + public void End() + { + Close(); + } + + public void Flush() + { + OutputStream.Flush(); + } + + public bool IsClosed { get; private set; } + + public void SetContentLength(long contentLength) {} + + public bool KeepAlive { get; set; } + + public Dictionary Items { get; private set; } + + public void SetCookie(Cookie cookie) + { + } + + public void ClearCookies() + { + } + } +} \ No newline at end of file diff --git a/ServiceStack/Host/RestHandler.cs b/ServiceStack/Host/RestHandler.cs new file mode 100644 index 000000000..5c360d150 --- /dev/null +++ b/ServiceStack/Host/RestHandler.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public class RestHandler + { + public string RequestName { get; set; } + + public async Task HandleResponseAsync(object response) + { + var taskResponse = response as Task; + + if (taskResponse == null) + { + return response; + } + + await taskResponse.ConfigureAwait(false); + + var taskResult = ServiceStackHost.Instance.GetTaskResult(taskResponse, RequestName); + + var taskResults = taskResult as Task[]; + + if (taskResults == null) + { + var subTask = taskResult as Task; + if (subTask != null) + taskResult = ServiceStackHost.Instance.GetTaskResult(subTask, RequestName); + + return taskResult; + } + + if (taskResults.Length == 0) + { + return new object[] { }; + } + + var firstResponse = ServiceStackHost.Instance.GetTaskResult(taskResults[0], RequestName); + var batchedResponses = firstResponse != null + ? (object[])Array.CreateInstance(firstResponse.GetType(), taskResults.Length) + : new object[taskResults.Length]; + batchedResponses[0] = firstResponse; + for (var i = 1; i < taskResults.Length; i++) + { + batchedResponses[i] = ServiceStackHost.Instance.GetTaskResult(taskResults[i], RequestName); + } + return batchedResponses; + } + + protected static object CreateContentTypeRequest(IRequest httpReq, Type requestType, string contentType) + { + if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) + { + var deserializer = ContentTypes.Instance.GetStreamDeserializer(contentType); + if (deserializer != null) + { + return deserializer(requestType, httpReq.InputStream); + } + } + return ServiceStackHost.Instance.CreateInstance(requestType); //Return an empty DTO, even for empty request bodies + } + + protected static object GetCustomRequestFromBinder(IRequest httpReq, Type requestType) + { + Func requestFactoryFn; + ServiceStackHost.Instance.ServiceController.RequestTypeFactoryMap.TryGetValue( + requestType, out requestFactoryFn); + + return requestFactoryFn != null ? requestFactoryFn(httpReq) : null; + } + + public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType) + { + pathInfo = GetSanitizedPathInfo(pathInfo, out contentType); + + return ServiceStackHost.Instance.ServiceController.GetRestPathForRequest(httpMethod, pathInfo); + } + + public static string GetSanitizedPathInfo(string pathInfo, out string contentType) + { + contentType = null; + var pos = pathInfo.LastIndexOf('.'); + if (pos >= 0) + { + var format = pathInfo.Substring(pos + 1); + contentType = GetFormatContentType(format); + if (contentType != null) + { + pathInfo = pathInfo.Substring(0, pos); + } + } + return pathInfo; + } + + private static string GetFormatContentType(string format) + { + //built-in formats + if (format == "json") + return "application/json"; + if (format == "xml") + return "application/xml"; + + return null; + } + + public RestPath GetRestPath(string httpMethod, string pathInfo) + { + if (this.RestPath == null) + { + string contentType; + this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out contentType); + + if (contentType != null) + ResponseContentType = contentType; + } + return this.RestPath; + } + + public RestPath RestPath { get; set; } + + // Set from SSHHF.GetHandlerForPathInfo() + public string ResponseContentType { get; set; } + + public async Task ProcessRequestAsync(IRequest httpReq, IResponse httpRes, string operationName) + { + var appHost = ServiceStackHost.Instance; + + var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo); + if (restPath == null) + { + throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo); + } + httpReq.SetRoute(restPath); + + if (ResponseContentType != null) + httpReq.ResponseContentType = ResponseContentType; + + var request = httpReq.Dto = CreateRequest(httpReq, restPath); + + if (appHost.ApplyRequestFilters(httpReq, httpRes, request)) + return; + + var rawResponse = await ServiceStackHost.Instance.ServiceController.Execute(request, httpReq).ConfigureAwait(false); + + if (httpRes.IsClosed) + return; + + var response = await HandleResponseAsync(rawResponse).ConfigureAwait(false); + + if (appHost.ApplyResponseFilters(httpReq, httpRes, response)) + return; + + await httpRes.WriteToResponse(httpReq, response).ConfigureAwait(false); + } + + public static object CreateRequest(IRequest httpReq, RestPath restPath) + { + var dtoFromBinder = GetCustomRequestFromBinder(httpReq, restPath.RequestType); + if (dtoFromBinder != null) + return dtoFromBinder; + + var requestParams = httpReq.GetFlattenedRequestParams(); + return CreateRequest(httpReq, restPath, requestParams); + } + + public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary requestParams) + { + var requestDto = CreateContentTypeRequest(httpReq, restPath.RequestType, httpReq.ContentType); + + return CreateRequest(httpReq, restPath, requestParams, requestDto); + } + + public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary requestParams, object requestDto) + { + string contentType; + var pathInfo = !restPath.IsWildCardPath + ? GetSanitizedPathInfo(httpReq.PathInfo, out contentType) + : httpReq.PathInfo; + + return restPath.CreateRequest(pathInfo, requestParams, requestDto); + } + + /// + /// Used in Unit tests + /// + /// + public object CreateRequest(IRequest httpReq, string operationName) + { + if (this.RestPath == null) + throw new ArgumentNullException("No RestPath found"); + + return CreateRequest(httpReq, this.RestPath); + } + } + +} diff --git a/ServiceStack/Host/RestPath.cs b/ServiceStack/Host/RestPath.cs new file mode 100644 index 000000000..7222578a9 --- /dev/null +++ b/ServiceStack/Host/RestPath.cs @@ -0,0 +1,443 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using ServiceStack.Serialization; + +namespace ServiceStack.Host +{ + public class RestPath + { + private const string WildCard = "*"; + private const char WildCardChar = '*'; + private const string PathSeperator = "/"; + private const char PathSeperatorChar = '/'; + private const char ComponentSeperator = '.'; + private const string VariablePrefix = "{"; + + readonly bool[] componentsWithSeparators; + + private readonly string restPath; + private readonly string allowedVerbs; + private readonly bool allowsAllVerbs; + public bool IsWildCardPath { get; private set; } + + private readonly string[] literalsToMatch; + + private readonly string[] variablesNames; + + private readonly bool[] isWildcard; + private readonly int wildcardCount = 0; + + public int VariableArgsCount { get; set; } + + /// + /// The number of segments separated by '/' determinable by path.Split('/').Length + /// e.g. /path/to/here.ext == 3 + /// + public int PathComponentsCount { get; set; } + + /// + /// The total number of segments after subparts have been exploded ('.') + /// e.g. /path/to/here.ext == 4 + /// + public int TotalComponentsCount { get; set; } + + public string[] Verbs + { + get + { + return allowsAllVerbs + ? new[] { ActionContext.AnyAction } + : AllowedVerbs.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + public Type RequestType { get; private set; } + + public string Path { get { return this.restPath; } } + + public string Summary { get; private set; } + + public string Notes { get; private set; } + + public bool AllowsAllVerbs { get { return this.allowsAllVerbs; } } + + public string AllowedVerbs { get { return this.allowedVerbs; } } + + public int Priority { get; set; } //passed back to RouteAttribute + + public static string[] GetPathPartsForMatching(string pathInfo) + { + var parts = pathInfo.ToLower().Split(PathSeperatorChar) + .Where(x => !string.IsNullOrEmpty(x)).ToArray(); + return parts; + } + + public static IEnumerable GetFirstMatchHashKeys(string[] pathPartsForMatching) + { + var hashPrefix = pathPartsForMatching.Length + PathSeperator; + return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); + } + + public static IEnumerable GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching) + { + const string hashPrefix = WildCard + PathSeperator; + return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); + } + + private static IEnumerable GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching) + { + foreach (var part in pathPartsForMatching) + { + yield return hashPrefix + part; + var subParts = part.Split(ComponentSeperator); + if (subParts.Length == 1) continue; + + foreach (var subPart in subParts) + { + yield return hashPrefix + subPart; + } + } + } + + public RestPath(Type requestType, string path, string verbs, string summary = null, string notes = null) + { + this.RequestType = requestType; + this.Summary = summary; + this.Notes = notes; + this.restPath = path; + + this.allowsAllVerbs = verbs == null || verbs == WildCard; + if (!this.allowsAllVerbs) + { + this.allowedVerbs = verbs.ToUpper(); + } + + var componentsList = new List(); + + //We only split on '.' if the restPath has them. Allows for /{action}.{type} + var hasSeparators = new List(); + foreach (var component in this.restPath.Split(PathSeperatorChar)) + { + if (string.IsNullOrEmpty(component)) continue; + + if (component.Contains(VariablePrefix) + && component.IndexOf(ComponentSeperator) != -1) + { + hasSeparators.Add(true); + componentsList.AddRange(component.Split(ComponentSeperator)); + } + else + { + hasSeparators.Add(false); + componentsList.Add(component); + } + } + + var components = componentsList.ToArray(); + this.TotalComponentsCount = components.Length; + + this.literalsToMatch = new string[this.TotalComponentsCount]; + this.variablesNames = new string[this.TotalComponentsCount]; + this.isWildcard = new bool[this.TotalComponentsCount]; + this.componentsWithSeparators = hasSeparators.ToArray(); + this.PathComponentsCount = this.componentsWithSeparators.Length; + string firstLiteralMatch = null; + + var sbHashKey = new StringBuilder(); + for (var i = 0; i < components.Length; i++) + { + var component = components[i]; + + if (component.StartsWith(VariablePrefix)) + { + var variableName = component.Substring(1, component.Length - 2); + if (variableName[variableName.Length - 1] == WildCardChar) + { + this.isWildcard[i] = true; + variableName = variableName.Substring(0, variableName.Length - 1); + } + this.variablesNames[i] = variableName; + this.VariableArgsCount++; + } + else + { + this.literalsToMatch[i] = component.ToLower(); + sbHashKey.Append(i + PathSeperatorChar.ToString() + this.literalsToMatch); + + if (firstLiteralMatch == null) + { + firstLiteralMatch = this.literalsToMatch[i]; + } + } + } + + for (var i = 0; i < components.Length - 1; i++) + { + if (!this.isWildcard[i]) continue; + if (this.literalsToMatch[i + 1] == null) + { + throw new ArgumentException( + "A wildcard path component must be at the end of the path or followed by a literal path component."); + } + } + + this.wildcardCount = this.isWildcard.Count(x => x); + this.IsWildCardPath = this.wildcardCount > 0; + + this.FirstMatchHashKey = !this.IsWildCardPath + ? this.PathComponentsCount + PathSeperator + firstLiteralMatch + : WildCardChar + PathSeperator + firstLiteralMatch; + + this.IsValid = sbHashKey.Length > 0; + this.UniqueMatchHashKey = sbHashKey.ToString(); + + this.typeDeserializer = new StringMapTypeDeserializer(this.RequestType); + RegisterCaseInsenstivePropertyNameMappings(); + } + + private void RegisterCaseInsenstivePropertyNameMappings() + { + foreach (var propertyInfo in RequestType.GetSerializableProperties()) + { + var propertyName = propertyInfo.Name; + propertyNamesMap.Add(propertyName.ToLower(), propertyName); + } + } + + public bool IsValid { get; set; } + + /// + /// Provide for quick lookups based on hashes that can be determined from a request url + /// + public string FirstMatchHashKey { get; private set; } + + public string UniqueMatchHashKey { get; private set; } + + private readonly StringMapTypeDeserializer typeDeserializer; + + private readonly Dictionary propertyNamesMap = new Dictionary(); + + public static Func CalculateMatchScore { get; set; } + + public int MatchScore(string httpMethod, string[] withPathInfoParts) + { + if (CalculateMatchScore != null) + return CalculateMatchScore(this, httpMethod, withPathInfoParts); + + int wildcardMatchCount; + var isMatch = IsMatch(httpMethod, withPathInfoParts, out wildcardMatchCount); + if (!isMatch) return -1; + + var score = 0; + + //Routes with least wildcard matches get the highest score + score += Math.Max((100 - wildcardMatchCount), 1) * 1000; + + //Routes with less variable (and more literal) matches + score += Math.Max((10 - VariableArgsCount), 1) * 100; + + //Exact verb match is better than ANY + var exactVerb = httpMethod == AllowedVerbs; + score += exactVerb ? 10 : 1; + + return score; + } + + /// + /// For performance withPathInfoParts should already be a lower case string + /// to minimize redundant matching operations. + /// + /// + /// + /// + /// + public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount) + { + wildcardMatchCount = 0; + + if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) return false; + if (!this.allowsAllVerbs && !this.allowedVerbs.Contains(httpMethod.ToUpper())) return false; + + if (!ExplodeComponents(ref withPathInfoParts)) return false; + if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) return false; + + int pathIx = 0; + for (var i = 0; i < this.TotalComponentsCount; i++) + { + if (this.isWildcard[i]) + { + if (i < this.TotalComponentsCount - 1) + { + // Continue to consume up until a match with the next literal + while (pathIx < withPathInfoParts.Length && withPathInfoParts[pathIx] != this.literalsToMatch[i + 1]) + { + pathIx++; + wildcardMatchCount++; + } + + // Ensure there are still enough parts left to match the remainder + if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1)) + { + return false; + } + } + else + { + // A wildcard at the end matches the remainder of path + wildcardMatchCount += withPathInfoParts.Length - pathIx; + pathIx = withPathInfoParts.Length; + } + } + else + { + var literalToMatch = this.literalsToMatch[i]; + if (literalToMatch == null) + { + // Matching an ordinary (non-wildcard) variable consumes a single part + pathIx++; + continue; + } + + if (withPathInfoParts.Length <= pathIx || withPathInfoParts[pathIx] != literalToMatch) return false; + pathIx++; + } + } + + return pathIx == withPathInfoParts.Length; + } + + private bool ExplodeComponents(ref string[] withPathInfoParts) + { + var totalComponents = new List(); + for (var i = 0; i < withPathInfoParts.Length; i++) + { + var component = withPathInfoParts[i]; + if (string.IsNullOrEmpty(component)) continue; + + if (this.PathComponentsCount != this.TotalComponentsCount + && this.componentsWithSeparators[i]) + { + var subComponents = component.Split(ComponentSeperator); + if (subComponents.Length < 2) return false; + totalComponents.AddRange(subComponents); + } + else + { + totalComponents.Add(component); + } + } + + withPathInfoParts = totalComponents.ToArray(); + return true; + } + + public object CreateRequest(string pathInfo, Dictionary queryStringAndFormData, object fromInstance) + { + var requestComponents = pathInfo.Split(PathSeperatorChar) + .Where(x => !string.IsNullOrEmpty(x)).ToArray(); + + ExplodeComponents(ref requestComponents); + + if (requestComponents.Length != this.TotalComponentsCount) + { + var isValidWildCardPath = this.IsWildCardPath + && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; + + if (!isValidWildCardPath) + throw new ArgumentException(string.Format( + "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", + pathInfo, this.restPath)); + } + + var requestKeyValuesMap = new Dictionary(); + var pathIx = 0; + for (var i = 0; i < this.TotalComponentsCount; i++) + { + var variableName = this.variablesNames[i]; + if (variableName == null) + { + pathIx++; + continue; + } + + string propertyNameOnRequest; + if (!this.propertyNamesMap.TryGetValue(variableName.ToLower(), out propertyNameOnRequest)) + { + if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) + { + pathIx++; + continue; + } + + throw new ArgumentException("Could not find property " + + variableName + " on " + RequestType.GetOperationName()); + } + + var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch + if (value != null && this.isWildcard[i]) + { + if (i == this.TotalComponentsCount - 1) + { + // Wildcard at end of path definition consumes all the rest + var sb = new StringBuilder(); + sb.Append(value); + for (var j = pathIx + 1; j < requestComponents.Length; j++) + { + sb.Append(PathSeperatorChar + requestComponents[j]); + } + value = sb.ToString(); + } + else + { + // Wildcard in middle of path definition consumes up until it + // hits a match for the next element in the definition (which must be a literal) + // It may consume 0 or more path parts + var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; + if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) + { + var sb = new StringBuilder(); + sb.Append(value); + pathIx++; + while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) + { + sb.Append(PathSeperatorChar + requestComponents[pathIx++]); + } + value = sb.ToString(); + } + else + { + value = null; + } + } + } + else + { + // Variable consumes single path item + pathIx++; + } + + requestKeyValuesMap[propertyNameOnRequest] = value; + } + + if (queryStringAndFormData != null) + { + //Query String and form data can override variable path matches + //path variables < query string < form data + foreach (var name in queryStringAndFormData) + { + requestKeyValuesMap[name.Key] = name.Value; + } + } + + return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); + } + + public override int GetHashCode() + { + return UniqueMatchHashKey.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/ServiceStack/Host/ServiceController.cs b/ServiceStack/Host/ServiceController.cs new file mode 100644 index 000000000..703f06365 --- /dev/null +++ b/ServiceStack/Host/ServiceController.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public delegate Task InstanceExecFn(IRequest requestContext, object intance, object request); + public delegate object ActionInvokerFn(object intance, object request); + public delegate void VoidActionInvokerFn(object intance, object request); + + public class ServiceController + { + private readonly Func> _resolveServicesFn; + + public ServiceController(Func> resolveServicesFn) + { + _resolveServicesFn = resolveServicesFn; + this.RequestTypeFactoryMap = new Dictionary>(); + } + + public Dictionary> RequestTypeFactoryMap { get; set; } + + public void Init() + { + foreach (var serviceType in _resolveServicesFn()) + { + RegisterService(serviceType); + } + } + + private Type[] GetGenericArguments(Type type) + { + return type.GetTypeInfo().IsGenericTypeDefinition + ? type.GetTypeInfo().GenericTypeParameters + : type.GetTypeInfo().GenericTypeArguments; + } + + public void RegisterService(Type serviceType) + { + var processedReqs = new HashSet(); + + var actions = ServiceExecGeneral.Reset(serviceType); + + var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); + + var appHost = ServiceStackHost.Instance; + foreach (var mi in serviceType.GetActions()) + { + var requestType = mi.GetParameters()[0].ParameterType; + if (processedReqs.Contains(requestType)) continue; + processedReqs.Add(requestType); + + ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); + + var returnMarker = requestType.GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>)); + var responseType = returnMarker != null ? + GetGenericArguments(returnMarker)[0] + : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ? + mi.ReturnType + : Type.GetType(requestType.FullName + "Response"); + + RegisterRestPaths(requestType); + + appHost.Metadata.Add(serviceType, requestType, responseType); + + if (requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo())) + { + this.RequestTypeFactoryMap[requestType] = req => + { + var restPath = req.GetRoute(); + var request = RestHandler.CreateRequest(req, restPath, req.GetRequestParams(), ServiceStackHost.Instance.CreateInstance(requestType)); + + var rawReq = (IRequiresRequestStream)request; + rawReq.RequestStream = req.InputStream; + return rawReq; + }; + } + } + } + + public readonly Dictionary> RestPathMap = new Dictionary>(); + + public void RegisterRestPaths(Type requestType) + { + var appHost = ServiceStackHost.Instance; + var attrs = appHost.GetRouteAttributes(requestType); + foreach (MediaBrowser.Model.Services.RouteAttribute attr in attrs) + { + var restPath = new RestPath(requestType, attr.Path, attr.Verbs, attr.Summary, attr.Notes); + + if (!restPath.IsValid) + throw new NotSupportedException(string.Format( + "RestPath '{0}' on Type '{1}' is not Valid", attr.Path, requestType.GetOperationName())); + + RegisterRestPath(restPath); + } + } + + private static readonly char[] InvalidRouteChars = new[] { '?', '&' }; + + public void RegisterRestPath(RestPath restPath) + { + if (!restPath.Path.StartsWith("/")) + throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetOperationName())); + if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) + throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. " + + "See https://github.com/ServiceStack/ServiceStack/wiki/Routing for info on valid routes.", restPath.Path, restPath.RequestType.GetOperationName())); + + List pathsAtFirstMatch; + if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out pathsAtFirstMatch)) + { + pathsAtFirstMatch = new List(); + RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch; + } + pathsAtFirstMatch.Add(restPath); + } + + public void AfterInit() + { + var appHost = ServiceStackHost.Instance; + + //Register any routes configured on Metadata.Routes + foreach (var restPath in appHost.RestPaths) + { + RegisterRestPath(restPath); + } + + //Sync the RestPaths collections + appHost.RestPaths.Clear(); + appHost.RestPaths.AddRange(RestPathMap.Values.SelectMany(x => x)); + } + + public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) + { + var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo); + + List firstMatches; + + var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts); + foreach (var potentialHashMatch in yieldedHashMatches) + { + if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue; + + var bestScore = -1; + foreach (var restPath in firstMatches) + { + var score = restPath.MatchScore(httpMethod, matchUsingPathParts); + if (score > bestScore) bestScore = score; + } + if (bestScore > 0) + { + foreach (var restPath in firstMatches) + { + if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts)) + return restPath; + } + } + } + + var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); + foreach (var potentialHashMatch in yieldedWildcardMatches) + { + if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) continue; + + var bestScore = -1; + foreach (var restPath in firstMatches) + { + var score = restPath.MatchScore(httpMethod, matchUsingPathParts); + if (score > bestScore) bestScore = score; + } + if (bestScore > 0) + { + foreach (var restPath in firstMatches) + { + if (bestScore == restPath.MatchScore(httpMethod, matchUsingPathParts)) + return restPath; + } + } + } + + return null; + } + + public async Task Execute(object requestDto, IRequest req) + { + req.Dto = requestDto; + var requestType = requestDto.GetType(); + req.OperationName = requestType.Name; + + var serviceType = ServiceStackHost.Instance.Metadata.GetServiceTypeByRequest(requestType); + + var service = ServiceStackHost.Instance.CreateInstance(serviceType); + + //var service = typeFactory.CreateInstance(serviceType); + + var serviceRequiresContext = service as IRequiresRequest; + if (serviceRequiresContext != null) + { + serviceRequiresContext.Request = req; + } + + if (req.Dto == null) // Don't override existing batched DTO[] + req.Dto = requestDto; + + //Executes the service and returns the result + var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetOperationName()).ConfigureAwait(false); + + if (req.Response.Dto == null) + req.Response.Dto = response; + + return response; + } + } + +} \ No newline at end of file diff --git a/ServiceStack/Host/ServiceExec.cs b/ServiceStack/Host/ServiceExec.cs new file mode 100644 index 000000000..cb501a3ad --- /dev/null +++ b/ServiceStack/Host/ServiceExec.cs @@ -0,0 +1,156 @@ +//Copyright (c) Service Stack LLC. All Rights Reserved. +//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace ServiceStack.Host +{ + public static class ServiceExecExtensions + { + public static IEnumerable GetActions(this Type serviceType) + { + foreach (var mi in serviceType.GetRuntimeMethods().Where(i => i.IsPublic && !i.IsStatic)) + { + if (mi.GetParameters().Length != 1) + continue; + + var actionName = mi.Name.ToUpper(); + if (!HttpMethods.AllVerbs.Contains(actionName) && actionName != ActionContext.AnyAction) + continue; + + yield return mi; + } + } + } + + internal static class ServiceExecGeneral + { + public static Dictionary execMap = new Dictionary(); + + public static void CreateServiceRunnersFor(Type requestType, List actions) + { + foreach (var actionCtx in actions) + { + if (execMap.ContainsKey(actionCtx.Id)) continue; + + execMap[actionCtx.Id] = actionCtx; + } + } + + public static async Task Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) + { + var actionName = request.Verb + ?? HttpMethods.Post; //MQ Services + + ActionContext actionContext; + if (ServiceExecGeneral.execMap.TryGetValue(ActionContext.Key(serviceType, actionName, requestName), out actionContext) + || ServiceExecGeneral.execMap.TryGetValue(ActionContext.AnyKey(serviceType, requestName), out actionContext)) + { + if (actionContext.RequestFilters != null) + { + foreach (var requestFilter in actionContext.RequestFilters) + { + requestFilter.RequestFilter(request, request.Response, requestDto); + if (request.Response.IsClosed) return null; + } + } + + var response = actionContext.ServiceAction(instance, requestDto); + + var taskResponse = response as Task; + if (taskResponse != null) + { + await taskResponse.ConfigureAwait(false); + response = ServiceStackHost.Instance.GetTaskResult(taskResponse, requestName); + } + + return response; + } + + var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLower(); + throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetOperationName(), expectedMethodName, serviceType.GetOperationName())); + } + + public static List Reset(Type serviceType) + { + var actions = new List(); + + foreach (var mi in serviceType.GetActions()) + { + var actionName = mi.Name.ToUpper(); + var args = mi.GetParameters(); + + var requestType = args[0].ParameterType; + var actionCtx = new ActionContext + { + Id = ActionContext.Key(serviceType, actionName, requestType.GetOperationName()) + }; + + try + { + actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); + } + catch + { + //Potential problems with MONO, using reflection for fallback + actionCtx.ServiceAction = (service, request) => + mi.Invoke(service, new[] { request }); + } + + var reqFilters = new List(); + + foreach (var attr in mi.GetCustomAttributes(true)) + { + var hasReqFilter = attr as IHasRequestFilter; + + if (hasReqFilter != null) + reqFilters.Add(hasReqFilter); + } + + if (reqFilters.Count > 0) + actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); + + actions.Add(actionCtx); + } + + return actions; + } + + private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi) + { + var serviceParam = Expression.Parameter(typeof(object), "serviceObj"); + var serviceStrong = Expression.Convert(serviceParam, serviceType); + + var requestDtoParam = Expression.Parameter(typeof(object), "requestDto"); + var requestDtoStrong = Expression.Convert(requestDtoParam, requestType); + + Expression callExecute = Expression.Call( + serviceStrong, mi, requestDtoStrong); + + if (mi.ReturnType != typeof(void)) + { + var executeFunc = Expression.Lambda + (callExecute, serviceParam, requestDtoParam).Compile(); + + return executeFunc; + } + else + { + var executeFunc = Expression.Lambda + (callExecute, serviceParam, requestDtoParam).Compile(); + + return (service, request) => + { + executeFunc(service, request); + return null; + }; + } + } + } +} \ No newline at end of file diff --git a/ServiceStack/Host/ServiceMetadata.cs b/ServiceStack/Host/ServiceMetadata.cs new file mode 100644 index 000000000..240e6f32d --- /dev/null +++ b/ServiceStack/Host/ServiceMetadata.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace ServiceStack.Host +{ + public class ServiceMetadata + { + public ServiceMetadata() + { + this.OperationsMap = new Dictionary(); + } + + public Dictionary OperationsMap { get; protected set; } + + public void Add(Type serviceType, Type requestType, Type responseType) + { + this.OperationsMap[requestType] = serviceType; + } + + public Type GetServiceTypeByRequest(Type requestType) + { + Type serviceType; + OperationsMap.TryGetValue(requestType, out serviceType); + return serviceType; + } + } +} diff --git a/ServiceStack/HttpHandlerFactory.cs b/ServiceStack/HttpHandlerFactory.cs new file mode 100644 index 000000000..d48bfeb5f --- /dev/null +++ b/ServiceStack/HttpHandlerFactory.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public class HttpHandlerFactory + { + // Entry point for HttpListener + public static RestHandler GetHandler(IHttpRequest httpReq) + { + var pathInfo = httpReq.PathInfo; + + var pathParts = pathInfo.TrimStart('/').Split('/'); + if (pathParts.Length == 0) return null; + + string contentType; + var restPath = RestHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out contentType); + if (restPath != null) + return new RestHandler { RestPath = restPath, RequestName = restPath.RequestType.GetOperationName(), ResponseContentType = contentType }; + + return null; + } + } +} \ No newline at end of file diff --git a/ServiceStack/HttpRequestExtensions.cs b/ServiceStack/HttpRequestExtensions.cs new file mode 100644 index 000000000..c34d62601 --- /dev/null +++ b/ServiceStack/HttpRequestExtensions.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public static class HttpRequestExtensions + { + /** + * + Input: http://localhost:96/Cambia3/Temp/Test.aspx/path/info?q=item#fragment + + Some HttpRequest path and URL properties: + Request.ApplicationPath: /Cambia3 + Request.CurrentExecutionFilePath: /Cambia3/Temp/Test.aspx + Request.FilePath: /Cambia3/Temp/Test.aspx + Request.Path: /Cambia3/Temp/Test.aspx/path/info + Request.PathInfo: /path/info + Request.PhysicalApplicationPath: D:\Inetpub\wwwroot\CambiaWeb\Cambia3\ + Request.QueryString: /Cambia3/Temp/Test.aspx/path/info?query=arg + Request.Url.AbsolutePath: /Cambia3/Temp/Test.aspx/path/info + Request.Url.AbsoluteUri: http://localhost:96/Cambia3/Temp/Test.aspx/path/info?query=arg + Request.Url.Fragment: + Request.Url.Host: localhost + Request.Url.LocalPath: /Cambia3/Temp/Test.aspx/path/info + Request.Url.PathAndQuery: /Cambia3/Temp/Test.aspx/path/info?query=arg + Request.Url.Port: 96 + Request.Url.Query: ?query=arg + Request.Url.Scheme: http + Request.Url.Segments: / + Cambia3/ + Temp/ + Test.aspx/ + path/ + info + * */ + + /// + /// Duplicate Params are given a unique key by appending a #1 suffix + /// + public static Dictionary GetRequestParams(this IRequest request) + { + var map = new Dictionary(); + + foreach (var name in request.QueryString.Keys) + { + if (name == null) continue; //thank you ASP.NET + + var values = request.QueryString.GetValues(name); + if (values.Length == 1) + { + map[name] = values[0]; + } + else + { + for (var i = 0; i < values.Length; i++) + { + map[name + (i == 0 ? "" : "#" + i)] = values[i]; + } + } + } + + if ((request.Verb == HttpMethods.Post || request.Verb == HttpMethods.Put) + && request.FormData != null) + { + foreach (var name in request.FormData.Keys) + { + if (name == null) continue; //thank you ASP.NET + + var values = request.FormData.GetValues(name); + if (values.Length == 1) + { + map[name] = values[0]; + } + else + { + for (var i = 0; i < values.Length; i++) + { + map[name + (i == 0 ? "" : "#" + i)] = values[i]; + } + } + } + } + + return map; + } + + /// + /// Duplicate params have their values joined together in a comma-delimited string + /// + public static Dictionary GetFlattenedRequestParams(this IRequest request) + { + var map = new Dictionary(); + + foreach (var name in request.QueryString.Keys) + { + if (name == null) continue; //thank you ASP.NET + map[name] = request.QueryString[name]; + } + + if ((request.Verb == HttpMethods.Post || request.Verb == HttpMethods.Put) + && request.FormData != null) + { + foreach (var name in request.FormData.Keys) + { + if (name == null) continue; //thank you ASP.NET + map[name] = request.FormData[name]; + } + } + + return map; + } + + public static void SetRoute(this IRequest req, RestPath route) + { + req.Items["__route"] = route; + } + + public static RestPath GetRoute(this IRequest req) + { + object route; + req.Items.TryGetValue("__route", out route); + return route as RestPath; + } + } +} \ No newline at end of file diff --git a/ServiceStack/HttpResponseExtensionsInternal.cs b/ServiceStack/HttpResponseExtensionsInternal.cs new file mode 100644 index 000000000..1195f63ca --- /dev/null +++ b/ServiceStack/HttpResponseExtensionsInternal.cs @@ -0,0 +1,237 @@ +//Copyright (c) Service Stack LLC. All Rights Reserved. +//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public static class HttpResponseExtensionsInternal + { + public static async Task WriteToOutputStream(IResponse response, object result) + { + var asyncStreamWriter = result as IAsyncStreamWriter; + if (asyncStreamWriter != null) + { + await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false); + return true; + } + + var streamWriter = result as IStreamWriter; + if (streamWriter != null) + { + streamWriter.WriteTo(response.OutputStream); + return true; + } + + var stream = result as Stream; + if (stream != null) + { + WriteTo(stream, response.OutputStream); + return true; + } + + var bytes = result as byte[]; + if (bytes != null) + { + response.ContentType = "application/octet-stream"; + response.SetContentLength(bytes.Length); + + response.OutputStream.Write(bytes, 0, bytes.Length); + return true; + } + + return false; + } + + public static long WriteTo(Stream inStream, Stream outStream) + { + var memoryStream = inStream as MemoryStream; + if (memoryStream != null) + { + memoryStream.WriteTo(outStream); + return memoryStream.Position; + } + + var data = new byte[4096]; + long total = 0; + int bytesRead; + + while ((bytesRead = inStream.Read(data, 0, data.Length)) > 0) + { + outStream.Write(data, 0, bytesRead); + total += bytesRead; + } + + return total; + } + + /// + /// End a ServiceStack Request with no content + /// + public static void EndRequestWithNoContent(this IResponse httpRes) + { + if (httpRes.StatusCode == (int)HttpStatusCode.OK) + { + httpRes.StatusCode = (int)HttpStatusCode.NoContent; + } + + httpRes.SetContentLength(0); + } + + public static Task WriteToResponse(this IResponse httpRes, MediaBrowser.Model.Services.IRequest httpReq, object result) + { + if (result == null) + { + httpRes.EndRequestWithNoContent(); + return Task.FromResult(true); + } + + var httpResult = result as IHttpResult; + if (httpResult != null) + { + httpResult.RequestContext = httpReq; + httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType; + var httpResSerializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType); + return httpRes.WriteToResponse(httpResult, httpResSerializer, httpReq); + } + + var serializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType); + return httpRes.WriteToResponse(result, serializer, httpReq); + } + + private static object GetDto(object response) + { + if (response == null) return null; + var httpResult = response as IHttpResult; + return httpResult != null ? httpResult.Response : response; + } + + /// + /// Writes to response. + /// Response headers are customizable by implementing IHasHeaders an returning Dictionary of Http headers. + /// + /// The response. + /// Whether or not it was implicity handled by ServiceStack's built-in handlers. + /// The default action. + /// The serialization context. + /// + public static async Task WriteToResponse(this IResponse response, object result, Action defaultAction, MediaBrowser.Model.Services.IRequest request) + { + var defaultContentType = request.ResponseContentType; + if (result == null) + { + response.EndRequestWithNoContent(); + return; + } + + var httpResult = result as IHttpResult; + if (httpResult != null) + { + if (httpResult.RequestContext == null) + httpResult.RequestContext = request; + + response.Dto = response.Dto ?? GetDto(httpResult); + + response.StatusCode = httpResult.Status; + response.StatusDescription = httpResult.StatusDescription ?? httpResult.StatusCode.ToString(); + if (string.IsNullOrEmpty(httpResult.ContentType)) + { + httpResult.ContentType = defaultContentType; + } + response.ContentType = httpResult.ContentType; + + if (httpResult.Cookies != null) + { + var httpRes = response as IHttpResponse; + if (httpRes != null) + { + foreach (var cookie in httpResult.Cookies) + { + httpRes.SetCookie(cookie); + } + } + } + } + else + { + response.Dto = result; + } + + /* Mono Error: Exception: Method not found: 'System.Web.HttpResponse.get_Headers' */ + var responseOptions = result as IHasHeaders; + if (responseOptions != null) + { + //Reserving options with keys in the format 'xx.xxx' (No Http headers contain a '.' so its a safe restriction) + const string reservedOptions = "."; + + foreach (var responseHeaders in responseOptions.Headers) + { + if (responseHeaders.Key.Contains(reservedOptions)) continue; + if (responseHeaders.Key == "Content-Length") + { + response.SetContentLength(long.Parse(responseHeaders.Value)); + continue; + } + + response.AddHeader(responseHeaders.Key, responseHeaders.Value); + } + } + + //ContentType='text/html' is the default for a HttpResponse + //Do not override if another has been set + if (response.ContentType == null || response.ContentType == "text/html") + { + response.ContentType = defaultContentType; + } + + if (new HashSet { "application/json", }.Contains(response.ContentType)) + { + response.ContentType += "; charset=utf-8"; + } + + var disposableResult = result as IDisposable; + var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false); + if (writeToOutputStreamResult) + { + response.Flush(); //required for Compression + if (disposableResult != null) disposableResult.Dispose(); + return; + } + + if (httpResult != null) + result = httpResult.Response; + + var responseText = result as string; + if (responseText != null) + { + if (response.ContentType == null || response.ContentType == "text/html") + response.ContentType = defaultContentType; + response.Write(responseText); + + return; + } + + if (defaultAction == null) + { + throw new ArgumentNullException("defaultAction", String.Format( + "As result '{0}' is not a supported responseType, a defaultAction must be supplied", + (result != null ? result.GetType().GetOperationName() : ""))); + } + + + if (result != null) + defaultAction(request, result, response); + + if (disposableResult != null) + disposableResult.Dispose(); + } + + } +} diff --git a/ServiceStack/HttpResult.cs b/ServiceStack/HttpResult.cs new file mode 100644 index 000000000..23a5cdffb --- /dev/null +++ b/ServiceStack/HttpResult.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public class HttpResult + : IHttpResult, IAsyncStreamWriter + { + public HttpResult() + : this((object)null, null) + { + } + + public HttpResult(object response) + : this(response, null) + { + } + + public HttpResult(object response, string contentType) + : this(response, contentType, HttpStatusCode.OK) + { + } + + public HttpResult(HttpStatusCode statusCode, string statusDescription) + : this() + { + StatusCode = statusCode; + StatusDescription = statusDescription; + } + + public HttpResult(object response, HttpStatusCode statusCode) + : this(response, null, statusCode) + { } + + public HttpResult(object response, string contentType, HttpStatusCode statusCode) + { + this.Headers = new Dictionary(); + this.Cookies = new List(); + + this.Response = response; + this.ContentType = contentType; + this.StatusCode = statusCode; + } + + public HttpResult(Stream responseStream, string contentType) + : this(null, contentType, HttpStatusCode.OK) + { + this.ResponseStream = responseStream; + } + + public HttpResult(string responseText, string contentType) + : this(null, contentType, HttpStatusCode.OK) + { + this.ResponseText = responseText; + } + + public HttpResult(byte[] responseBytes, string contentType) + : this(null, contentType, HttpStatusCode.OK) + { + this.ResponseStream = new MemoryStream(responseBytes); + } + + public string ResponseText { get; private set; } + + public Stream ResponseStream { get; private set; } + + public string ContentType { get; set; } + + public IDictionary Headers { get; private set; } + + public List Cookies { get; private set; } + + public string ETag { get; set; } + + public TimeSpan? Age { get; set; } + + public TimeSpan? MaxAge { get; set; } + + public DateTime? Expires { get; set; } + + public DateTime? LastModified { get; set; } + + public Func ResultScope { get; set; } + + public string Location + { + set + { + if (StatusCode == HttpStatusCode.OK) + StatusCode = HttpStatusCode.Redirect; + + this.Headers["Location"] = value; + } + } + + public void SetPermanentCookie(string name, string value) + { + SetCookie(name, value, DateTime.UtcNow.AddYears(20), null); + } + + public void SetPermanentCookie(string name, string value, string path) + { + SetCookie(name, value, DateTime.UtcNow.AddYears(20), path); + } + + public void SetSessionCookie(string name, string value) + { + SetSessionCookie(name, value, null); + } + + public void SetSessionCookie(string name, string value, string path) + { + path = path ?? "/"; + this.Headers["Set-Cookie"] = string.Format("{0}={1};path=" + path, name, value); + } + + public void SetCookie(string name, string value, TimeSpan expiresIn, string path) + { + var expiresAt = DateTime.UtcNow.Add(expiresIn); + SetCookie(name, value, expiresAt, path); + } + + public void SetCookie(string name, string value, DateTime expiresAt, string path, bool secure = false, bool httpOnly = false) + { + path = path ?? "/"; + var cookie = string.Format("{0}={1};expires={2};path={3}", name, value, expiresAt.ToString("R"), path); + if (secure) + cookie += ";Secure"; + if (httpOnly) + cookie += ";HttpOnly"; + + this.Headers["Set-Cookie"] = cookie; + } + + public void DeleteCookie(string name) + { + var cookie = string.Format("{0}=;expires={1};path=/", name, DateTime.UtcNow.AddDays(-1).ToString("R")); + this.Headers["Set-Cookie"] = cookie; + } + + public int Status { get; set; } + + public HttpStatusCode StatusCode + { + get { return (HttpStatusCode)Status; } + set { Status = (int)value; } + } + + public string StatusDescription { get; set; } + + public object Response { get; set; } + + public MediaBrowser.Model.Services.IRequest RequestContext { get; set; } + + public string View { get; set; } + + public string Template { get; set; } + + public int PaddingLength { get; set; } + + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) + { + try + { + await WriteToInternalAsync(responseStream, cancellationToken).ConfigureAwait(false); + responseStream.Flush(); + } + finally + { + DisposeStream(); + } + } + + public static Task WriteTo(Stream inStream, Stream outStream, CancellationToken cancellationToken) + { + var memoryStream = inStream as MemoryStream; + if (memoryStream != null) + { + memoryStream.WriteTo(outStream); + return Task.FromResult(true); + } + + return inStream.CopyToAsync(outStream, 81920, cancellationToken); + } + + public async Task WriteToInternalAsync(Stream responseStream, CancellationToken cancellationToken) + { + var response = RequestContext != null ? RequestContext.Response : null; + + if (this.ResponseStream != null) + { + if (response != null) + { + var ms = ResponseStream as MemoryStream; + if (ms != null) + { + response.SetContentLength(ms.Length); + + await ms.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false); + return; + } + } + + await WriteTo(this.ResponseStream, responseStream, cancellationToken).ConfigureAwait(false); + return; + } + + if (this.ResponseText != null) + { + var bytes = Encoding.UTF8.GetBytes(this.ResponseText); + if (response != null) + response.SetContentLength(bytes.Length); + + await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + return; + } + + var bytesResponse = this.Response as byte[]; + if (bytesResponse != null) + { + if (response != null) + response.SetContentLength(bytesResponse.Length); + + await responseStream.WriteAsync(bytesResponse, 0, bytesResponse.Length).ConfigureAwait(false); + return; + } + + ContentTypes.Instance.SerializeToStream(this.RequestContext, this.Response, responseStream); + } + + private void DisposeStream() + { + try + { + if (ResponseStream != null) + { + this.ResponseStream.Dispose(); + } + } + catch { /*ignore*/ } + } + } +} diff --git a/ServiceStack/HttpUtils.cs b/ServiceStack/HttpUtils.cs new file mode 100644 index 000000000..41d191d61 --- /dev/null +++ b/ServiceStack/HttpUtils.cs @@ -0,0 +1,34 @@ +//Copyright (c) Service Stack LLC. All Rights Reserved. +//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + +using System; +using System.Collections.Generic; + +namespace ServiceStack +{ + internal static class HttpMethods + { + static readonly string[] allVerbs = new[] { + "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 + "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 + "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", + "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 + "ORDERPATCH", // RFC 3648 + "ACL", // RFC 3744 + "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ + "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ + "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", + "POLL", "SUBSCRIBE", "UNSUBSCRIBE" //MS Exchange WebDav: http://msdn.microsoft.com/en-us/library/aa142917.aspx + }; + + public static HashSet AllVerbs = new HashSet(allVerbs); + + public const string Get = "GET"; + public const string Put = "PUT"; + public const string Post = "POST"; + public const string Delete = "DELETE"; + public const string Options = "OPTIONS"; + public const string Head = "HEAD"; + public const string Patch = "PATCH"; + } +} diff --git a/ServiceStack/Properties/AssemblyInfo.cs b/ServiceStack/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6073dc0b4 --- /dev/null +++ b/ServiceStack/Properties/AssemblyInfo.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ServiceStack")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Service Stack LLC")] +[assembly: AssemblyProduct("ServiceStack")] +[assembly: AssemblyCopyright("Copyright (c) ServiceStack 2016")] +[assembly: AssemblyTrademark("Service Stack")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("06704d66-af8e-411f-8260-8d05de5ce6ad")] + +[assembly: AssemblyVersion("4.0.0.0")] +[assembly: AssemblyFileVersion("4.0.0.0")] diff --git a/ServiceStack/ReflectionExtensions.cs b/ServiceStack/ReflectionExtensions.cs new file mode 100644 index 000000000..bbabd0dd7 --- /dev/null +++ b/ServiceStack/ReflectionExtensions.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace ServiceStack +{ + public static class ReflectionExtensions + { + public static bool IsInstanceOf(this Type type, Type thisOrBaseType) + { + while (type != null) + { + if (type == thisOrBaseType) + return true; + + type = type.BaseType(); + } + return false; + } + + public static Type FirstGenericType(this Type type) + { + while (type != null) + { + if (type.IsGeneric()) + return type; + + type = type.BaseType(); + } + return null; + } + + public static Type GetTypeWithGenericTypeDefinitionOf(this Type type, Type genericTypeDefinition) + { + foreach (var t in type.GetTypeInterfaces()) + { + if (t.IsGeneric() && t.GetGenericTypeDefinition() == genericTypeDefinition) + { + return t; + } + } + + var genericType = type.FirstGenericType(); + if (genericType != null && genericType.GetGenericTypeDefinition() == genericTypeDefinition) + { + return genericType; + } + + return null; + } + + public static PropertyInfo[] GetAllProperties(this Type type) + { + if (type.IsInterface()) + { + var propertyInfos = new List(); + + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + + while (queue.Count > 0) + { + var subType = queue.Dequeue(); + foreach (var subInterface in subType.GetTypeInterfaces()) + { + if (considered.Contains(subInterface)) continue; + + considered.Add(subInterface); + queue.Enqueue(subInterface); + } + + var typeProperties = subType.GetTypesProperties(); + + var newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); + } + + return propertyInfos.ToArray(); + } + + return type.GetTypesProperties() + .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties + .ToArray(); + } + + public static PropertyInfo[] GetPublicProperties(this Type type) + { + if (type.IsInterface()) + { + var propertyInfos = new List(); + + var considered = new List(); + var queue = new Queue(); + considered.Add(type); + queue.Enqueue(type); + + while (queue.Count > 0) + { + var subType = queue.Dequeue(); + foreach (var subInterface in subType.GetTypeInterfaces()) + { + if (considered.Contains(subInterface)) continue; + + considered.Add(subInterface); + queue.Enqueue(subInterface); + } + + var typeProperties = subType.GetTypesPublicProperties(); + + var newPropertyInfos = typeProperties + .Where(x => !propertyInfos.Contains(x)); + + propertyInfos.InsertRange(0, newPropertyInfos); + } + + return propertyInfos.ToArray(); + } + + return type.GetTypesPublicProperties() + .Where(t => t.GetIndexParameters().Length == 0) // ignore indexed properties + .ToArray(); + } + + public const string DataMember = "DataMemberAttribute"; + + internal static string[] IgnoreAttributesNamed = new[] { + "IgnoreDataMemberAttribute", + "JsonIgnoreAttribute" + }; + + public static PropertyInfo[] GetSerializableProperties(this Type type) + { + var properties = type.IsDto() + ? type.GetAllProperties() + : type.GetPublicProperties(); + return properties.OnlySerializableProperties(type); + } + + + private static List _excludeTypes = new List { typeof(Stream) }; + + public static PropertyInfo[] OnlySerializableProperties(this PropertyInfo[] properties, Type type = null) + { + var isDto = type.IsDto(); + var readableProperties = properties.Where(x => x.PropertyGetMethod(nonPublic: isDto) != null); + + if (isDto) + { + return readableProperties.Where(attr => + attr.HasAttribute()).ToArray(); + } + + // else return those properties that are not decorated with IgnoreDataMember + return readableProperties + .Where(prop => prop.AllAttributes() + .All(attr => + { + var name = attr.GetType().Name; + return !IgnoreAttributesNamed.Contains(name); + })) + .Where(prop => !_excludeTypes.Contains(prop.PropertyType)) + .ToArray(); + } + } + + public static class PlatformExtensions //Because WinRT is a POS + { + public static bool IsInterface(this Type type) + { + return type.GetTypeInfo().IsInterface; + } + + public static bool IsGeneric(this Type type) + { + return type.GetTypeInfo().IsGenericType; + } + + public static Type BaseType(this Type type) + { + return type.GetTypeInfo().BaseType; + } + + public static Type[] GetTypeInterfaces(this Type type) + { + return type.GetTypeInfo().ImplementedInterfaces.ToArray(); + } + + internal static PropertyInfo[] GetTypesPublicProperties(this Type subType) + { + var pis = new List(); + foreach (var pi in subType.GetRuntimeProperties()) + { + var mi = pi.GetMethod ?? pi.SetMethod; + if (mi != null && mi.IsStatic) continue; + pis.Add(pi); + } + return pis.ToArray(); + } + + internal static PropertyInfo[] GetTypesProperties(this Type subType) + { + var pis = new List(); + foreach (var pi in subType.GetRuntimeProperties()) + { + var mi = pi.GetMethod ?? pi.SetMethod; + if (mi != null && mi.IsStatic) continue; + pis.Add(pi); + } + return pis.ToArray(); + } + + public static bool HasAttribute(this Type type) + { + return type.AllAttributes().Any(x => x.GetType() == typeof(T)); + } + + public static bool HasAttribute(this PropertyInfo pi) + { + return pi.AllAttributes().Any(x => x.GetType() == typeof(T)); + } + + public static bool IsDto(this Type type) + { + if (type == null) + return false; + + return type.HasAttribute(); + } + + public static MethodInfo PropertyGetMethod(this PropertyInfo pi, bool nonPublic = false) + { + return pi.GetMethod; + } + + public static object[] AllAttributes(this PropertyInfo propertyInfo) + { + return propertyInfo.GetCustomAttributes(true).ToArray(); + } + + public static object[] AllAttributes(this PropertyInfo propertyInfo, Type attrType) + { + return propertyInfo.GetCustomAttributes(true).Where(x => attrType.IsInstanceOf(x.GetType())).ToArray(); + } + + public static object[] AllAttributes(this Type type) + { + return type.GetTypeInfo().GetCustomAttributes(true).ToArray(); + } + + public static TAttr[] AllAttributes(this PropertyInfo pi) + { + return pi.AllAttributes(typeof(TAttr)).Cast().ToArray(); + } + + public static TAttr[] AllAttributes(this Type type) + where TAttr : Attribute + { + return type.GetTypeInfo().GetCustomAttributes(true).ToArray(); + } + } +} diff --git a/ServiceStack/ServiceStack.csproj b/ServiceStack/ServiceStack.csproj new file mode 100644 index 000000000..3402339a6 --- /dev/null +++ b/ServiceStack/ServiceStack.csproj @@ -0,0 +1,131 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {680A1709-25EB-4D52-A87F-EE03FFD94BAA} + Library + Properties + ServiceStack + ServiceStack + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + 512 + + + 3.5 + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + True + full + False + bin\Debug\ + TRACE;DEBUG;MONO + prompt + 4 + AllRules.ruleset + false + + + pdbonly + True + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + + + false + + + bin\Signed\ + TRACE + bin\Release\ServiceStack.XML + true + pdbonly + AnyCPU + prompt + AllRules.ruleset + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStack.nuget.targets b/ServiceStack/ServiceStack.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/ServiceStack/ServiceStack.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStack.xproj b/ServiceStack/ServiceStack.xproj new file mode 100644 index 000000000..ba8f8b8f2 --- /dev/null +++ b/ServiceStack/ServiceStack.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b2d733ab-620e-4c53-88a4-4b6638ab6a7a + ServiceStack + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.Runtime.cs b/ServiceStack/ServiceStackHost.Runtime.cs new file mode 100644 index 000000000..1a1656a0e --- /dev/null +++ b/ServiceStack/ServiceStackHost.Runtime.cs @@ -0,0 +1,74 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using MediaBrowser.Model.Services; +using ServiceStack.Support.WebHost; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost + { + /// + /// Applies the request filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// + /// + public virtual bool ApplyRequestFilters(IRequest req, IResponse res, object requestDto) + { + if (res.IsClosed) return res.IsClosed; + + //Exec all RequestFilter attributes with Priority < 0 + var attributes = FilterAttributeCache.GetRequestFilterAttributes(requestDto.GetType()); + var i = 0; + for (; i < attributes.Length && attributes[i].Priority < 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + if (res.IsClosed) return res.IsClosed; + + //Exec global filters + foreach (var requestFilter in GlobalRequestFilters) + { + requestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + //Exec remaining RequestFilter attributes with Priority >= 0 + for (; i < attributes.Length && attributes[i].Priority >= 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + + /// + /// Applies the response filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// + /// + public virtual bool ApplyResponseFilters(IRequest req, IResponse res, object response) + { + if (response != null) + { + if (res.IsClosed) return res.IsClosed; + } + + //Exec global filters + foreach (var responseFilter in GlobalResponseFilters) + { + responseFilter(req, res, response); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + } + +} \ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.cs b/ServiceStack/ServiceStackHost.cs new file mode 100644 index 000000000..8a1db25e4 --- /dev/null +++ b/ServiceStack/ServiceStackHost.cs @@ -0,0 +1,104 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost : IDisposable + { + public static ServiceStackHost Instance { get; protected set; } + + protected ServiceStackHost(string serviceName) + { + ServiceName = serviceName; + ServiceController = CreateServiceController(); + + RestPaths = new List(); + Metadata = new ServiceMetadata(); + GlobalRequestFilters = new List>(); + GlobalResponseFilters = new List>(); + } + + public abstract void Configure(); + + public abstract object CreateInstance(Type type); + + protected abstract ServiceController CreateServiceController(); + + public virtual ServiceStackHost Init() + { + Instance = this; + + ServiceController.Init(); + Configure(); + + ServiceController.AfterInit(); + + return this; + } + + public virtual ServiceStackHost Start(string urlBase) + { + throw new NotImplementedException("Start(listeningAtUrlBase) is not supported by this AppHost"); + } + + public string ServiceName { get; set; } + + public ServiceMetadata Metadata { get; set; } + + public ServiceController ServiceController { get; set; } + + public List RestPaths = new List(); + + public List> GlobalRequestFilters { get; set; } + + public List> GlobalResponseFilters { get; set; } + + public abstract T TryResolve(); + public abstract T Resolve(); + + public virtual MediaBrowser.Model.Services.RouteAttribute[] GetRouteAttributes(Type requestType) + { + return requestType.AllAttributes(); + } + + public abstract object GetTaskResult(Task task, string requestName); + + public abstract Func GetParseFn(Type propertyType); + + public abstract void SerializeToJson(object o, Stream stream); + public abstract void SerializeToXml(object o, Stream stream); + public abstract object DeserializeXml(Type type, Stream stream); + public abstract object DeserializeJson(Type type, Stream stream); + + public virtual void Dispose() + { + //JsConfig.Reset(); //Clears Runtime Attributes + + Instance = null; + } + + protected abstract ILogger Logger + { + get; + } + + public void OnLogError(Type type, string message) + { + Logger.Error(message); + } + + public void OnLogError(Type type, string message, Exception ex) + { + Logger.ErrorException(message, ex); + } + } +} diff --git a/ServiceStack/StringMapTypeDeserializer.cs b/ServiceStack/StringMapTypeDeserializer.cs new file mode 100644 index 000000000..762e8aaff --- /dev/null +++ b/ServiceStack/StringMapTypeDeserializer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Linq; +using System.Reflection; + +namespace ServiceStack.Serialization +{ + /// + /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) + /// + public class StringMapTypeDeserializer + { + internal class PropertySerializerEntry + { + public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn) + { + PropertySetFn = propertySetFn; + PropertyParseStringFn = propertyParseStringFn; + } + + public Action PropertySetFn; + public Func PropertyParseStringFn; + public Type PropertyType; + } + + private readonly Type type; + private readonly Dictionary propertySetterMap + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public Func GetParseFn(Type propertyType) + { + //Don't JSV-decode string values for string properties + if (propertyType == typeof(string)) + return s => s; + + return ServiceStackHost.Instance.GetParseFn(propertyType); + } + + public StringMapTypeDeserializer(Type type) + { + this.type = type; + + foreach (var propertyInfo in type.GetSerializableProperties()) + { + var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); + var propertyType = propertyInfo.PropertyType; + var propertyParseStringFn = GetParseFn(propertyType); + var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType }; + + var attr = propertyInfo.AllAttributes().FirstOrDefault(); + if (attr != null && attr.Name != null) + { + propertySetterMap[attr.Name] = propertySerializer; + } + propertySetterMap[propertyInfo.Name] = propertySerializer; + } + } + + public object PopulateFromMap(object instance, IDictionary keyValuePairs) + { + string propertyName = null; + string propertyTextValue = null; + PropertySerializerEntry propertySerializerEntry = null; + + if (instance == null) + instance = ServiceStackHost.Instance.CreateInstance(type); + + foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value))) + { + propertyName = pair.Key; + propertyTextValue = pair.Value; + + if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)) + { + if (propertyName == "v") + { + continue; + } + + continue; + } + + if (propertySerializerEntry.PropertySetFn == null) + { + continue; + } + + if (propertySerializerEntry.PropertyType == typeof(bool)) + { + //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value + propertyTextValue = LeftPart(propertyTextValue, ','); + } + + var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); + if (value == null) + { + continue; + } + propertySerializerEntry.PropertySetFn(instance, value); + } + + return instance; + } + + public static string LeftPart(string strVal, char needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } + + internal class TypeAccessor + { + public static Action GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Any()) return null; + + var setMethodInfo = propertyInfo.SetMethod; + return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); + } + } +} diff --git a/ServiceStack/UrlExtensions.cs b/ServiceStack/UrlExtensions.cs new file mode 100644 index 000000000..7b5a50ef1 --- /dev/null +++ b/ServiceStack/UrlExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace ServiceStack +{ + /// + /// Donated by Ivan Korneliuk from his post: + /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html + /// + /// Modified to only allow using routes matching the supplied HTTP Verb + /// + public static class UrlExtensions + { + public static string GetOperationName(this Type type) + { + var typeName = type.FullName != null //can be null, e.g. generic types + ? LeftPart(type.FullName, "[[") //Generic Fullname + .Replace(type.Namespace + ".", "") //Trim Namespaces + .Replace("+", ".") //Convert nested into normal type + : type.Name; + + return type.IsGenericParameter ? "'" + typeName : typeName; + } + + public static string LeftPart(string strVal, string needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } +} \ No newline at end of file diff --git a/ServiceStack/packages.config b/ServiceStack/packages.config new file mode 100644 index 000000000..6b8deb9c9 --- /dev/null +++ b/ServiceStack/packages.config @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ServiceStack/project.json b/ServiceStack/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/ServiceStack/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/ByteOrder.cs b/SocketHttpListener.Portable/ByteOrder.cs new file mode 100644 index 000000000..f5db52fd7 --- /dev/null +++ b/SocketHttpListener.Portable/ByteOrder.cs @@ -0,0 +1,17 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values that indicate whether the byte order is a Little-endian or Big-endian. + /// + public enum ByteOrder : byte + { + /// + /// Indicates a Little-endian. + /// + Little, + /// + /// Indicates a Big-endian. + /// + Big + } +} diff --git a/SocketHttpListener.Portable/CloseEventArgs.cs b/SocketHttpListener.Portable/CloseEventArgs.cs new file mode 100644 index 000000000..b1bb4b196 --- /dev/null +++ b/SocketHttpListener.Portable/CloseEventArgs.cs @@ -0,0 +1,90 @@ +using System; +using System.Text; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the WebSocket connection has been closed. + /// If you would like to get the reason for the close, you should access the or + /// property. + /// + public class CloseEventArgs : EventArgs + { + #region Private Fields + + private bool _clean; + private ushort _code; + private string _reason; + + #endregion + + #region Internal Constructors + + internal CloseEventArgs (PayloadData payload) + { + var data = payload.ApplicationData; + var len = data.Length; + _code = len > 1 + ? data.SubArray (0, 2).ToUInt16 (ByteOrder.Big) + : (ushort) CloseStatusCode.NoStatusCode; + + _reason = len > 2 + ? GetUtf8String(data.SubArray (2, len - 2)) + : String.Empty; + } + + private string GetUtf8String(byte[] bytes) + { + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code for the close. + /// + /// + /// A that represents the status code for the close if any. + /// + public ushort Code { + get { + return _code; + } + } + + /// + /// Gets the reason for the close. + /// + /// + /// A that represents the reason for the close if any. + /// + public string Reason { + get { + return _reason; + } + } + + /// + /// Gets a value indicating whether the WebSocket connection has been closed cleanly. + /// + /// + /// true if the WebSocket connection has been closed cleanly; otherwise, false. + /// + public bool WasClean { + get { + return _clean; + } + + internal set { + _clean = value; + } + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/CloseStatusCode.cs b/SocketHttpListener.Portable/CloseStatusCode.cs new file mode 100644 index 000000000..62a268bce --- /dev/null +++ b/SocketHttpListener.Portable/CloseStatusCode.cs @@ -0,0 +1,94 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the status code for the WebSocket connection close. + /// + /// + /// + /// The values of the status code are defined in + /// Section 7.4 + /// of RFC 6455. + /// + /// + /// "Reserved value" must not be set as a status code in a close control frame + /// by an endpoint. It's designated for use in applications expecting a status + /// code to indicate that the connection was closed due to the system grounds. + /// + /// + public enum CloseStatusCode : ushort + { + /// + /// Equivalent to close status 1000. + /// Indicates a normal close. + /// + Normal = 1000, + /// + /// Equivalent to close status 1001. + /// Indicates that an endpoint is going away. + /// + Away = 1001, + /// + /// Equivalent to close status 1002. + /// Indicates that an endpoint is terminating the connection due to a protocol error. + /// + ProtocolError = 1002, + /// + /// Equivalent to close status 1003. + /// Indicates that an endpoint is terminating the connection because it has received + /// an unacceptable type message. + /// + IncorrectData = 1003, + /// + /// Equivalent to close status 1004. + /// Still undefined. Reserved value. + /// + Undefined = 1004, + /// + /// Equivalent to close status 1005. + /// Indicates that no status code was actually present. Reserved value. + /// + NoStatusCode = 1005, + /// + /// Equivalent to close status 1006. + /// Indicates that the connection was closed abnormally. Reserved value. + /// + Abnormal = 1006, + /// + /// Equivalent to close status 1007. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that contains a data that isn't consistent with the type of the message. + /// + InconsistentData = 1007, + /// + /// Equivalent to close status 1008. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that violates its policy. + /// + PolicyViolation = 1008, + /// + /// Equivalent to close status 1009. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that is too big to process. + /// + TooBig = 1009, + /// + /// Equivalent to close status 1010. + /// Indicates that the client is terminating the connection because it has expected + /// the server to negotiate one or more extension, but the server didn't return them + /// in the handshake response. + /// + IgnoreExtension = 1010, + /// + /// Equivalent to close status 1011. + /// Indicates that the server is terminating the connection because it has encountered + /// an unexpected condition that prevented it from fulfilling the request. + /// + ServerError = 1011, + /// + /// Equivalent to close status 1015. + /// Indicates that the connection was closed due to a failure to perform a TLS handshake. + /// Reserved value. + /// + TlsHandshakeFailure = 1015 + } +} diff --git a/SocketHttpListener.Portable/CompressionMethod.cs b/SocketHttpListener.Portable/CompressionMethod.cs new file mode 100644 index 000000000..36a48d94c --- /dev/null +++ b/SocketHttpListener.Portable/CompressionMethod.cs @@ -0,0 +1,23 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the compression method used to compress the message on the WebSocket + /// connection. + /// + /// + /// The values of the compression method are defined in + /// Compression + /// Extensions for WebSocket. + /// + public enum CompressionMethod : byte + { + /// + /// Indicates non compression. + /// + None, + /// + /// Indicates using DEFLATE. + /// + Deflate + } +} diff --git a/SocketHttpListener.Portable/ErrorEventArgs.cs b/SocketHttpListener.Portable/ErrorEventArgs.cs new file mode 100644 index 000000000..bf1d6fc95 --- /dev/null +++ b/SocketHttpListener.Portable/ErrorEventArgs.cs @@ -0,0 +1,46 @@ +using System; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the gets an error. + /// If you would like to get the error message, you should access the + /// property. + /// + public class ErrorEventArgs : EventArgs + { + #region Private Fields + + private string _message; + + #endregion + + #region Internal Constructors + + internal ErrorEventArgs (string message) + { + _message = message; + } + + #endregion + + #region Public Properties + + /// + /// Gets the error message. + /// + /// + /// A that represents the error message. + /// + public string Message { + get { + return _message; + } + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Ext.cs b/SocketHttpListener.Portable/Ext.cs new file mode 100644 index 000000000..303263d0b --- /dev/null +++ b/SocketHttpListener.Portable/Ext.cs @@ -0,0 +1,1089 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; +using SocketHttpListener.Net; +using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; + +namespace SocketHttpListener +{ + /// + /// Provides a set of static methods for the websocket-sharp. + /// + public static class Ext + { + #region Private Const Fields + + private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; + + #endregion + + #region Private Methods + + private static MemoryStream compress(this Stream stream) + { + var output = new MemoryStream(); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) + { + stream.CopyTo(ds); + //ds.Close(); // "BFINAL" set to 1. + output.Position = 0; + + return output; + } + } + + private static byte[] decompress(this byte[] value) + { + if (value.Length == 0) + return value; + + using (var input = new MemoryStream(value)) + { + return input.decompressToArray(); + } + } + + private static MemoryStream decompress(this Stream stream) + { + var output = new MemoryStream(); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) + { + ds.CopyTo(output, true); + return output; + } + } + + private static byte[] decompressToArray(this Stream stream) + { + using (var decomp = stream.decompress()) + { + return decomp.ToArray(); + } + } + + private static byte[] readBytes(this Stream stream, byte[] buffer, int offset, int length) + { + var len = stream.Read(buffer, offset, length); + if (len < 1) + return buffer.SubArray(0, offset); + + var tmp = 0; + while (len < length) + { + tmp = stream.Read(buffer, offset + len, length - len); + if (tmp < 1) + break; + + len += tmp; + } + + return len < length + ? buffer.SubArray(0, offset + len) + : buffer; + } + + private static bool readBytes( + this Stream stream, byte[] buffer, int offset, int length, Stream dest) + { + var bytes = stream.readBytes(buffer, offset, length); + var len = bytes.Length; + dest.Write(bytes, 0, len); + + return len == offset + length; + } + + #endregion + + #region Internal Methods + + internal static byte[] Append(this ushort code, string reason) + { + using (var buffer = new MemoryStream()) + { + var tmp = code.ToByteArrayInternally(ByteOrder.Big); + buffer.Write(tmp, 0, 2); + if (reason != null && reason.Length > 0) + { + tmp = Encoding.UTF8.GetBytes(reason); + buffer.Write(tmp, 0, tmp.Length); + } + + return buffer.ToArray(); + } + } + + internal static string CheckIfClosable(this WebSocketState state) + { + return state == WebSocketState.Closing + ? "While closing the WebSocket connection." + : state == WebSocketState.Closed + ? "The WebSocket connection has already been closed." + : null; + } + + internal static string CheckIfOpen(this WebSocketState state) + { + return state == WebSocketState.Connecting + ? "A WebSocket connection isn't established." + : state == WebSocketState.Closing + ? "While closing the WebSocket connection." + : state == WebSocketState.Closed + ? "The WebSocket connection has already been closed." + : null; + } + + internal static string CheckIfValidControlData(this byte[] data, string paramName) + { + return data.Length > 125 + ? String.Format("'{0}' length must be less.", paramName) + : null; + } + + internal static string CheckIfValidSendData(this byte[] data) + { + return data == null + ? "'data' must not be null." + : null; + } + + internal static string CheckIfValidSendData(this string data) + { + return data == null + ? "'data' must not be null." + : null; + } + + internal static void Close(this HttpListenerResponse response, HttpStatusCode code) + { + response.StatusCode = (int)code; + response.OutputStream.Dispose(); + } + + internal static Stream Compress(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.compress() + : stream; + } + + internal static bool Contains(this IEnumerable source, Func condition) + { + foreach (T elm in source) + if (condition(elm)) + return true; + + return false; + } + + internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition) + { + var readLen = 0; + var bufferLen = 256; + var buffer = new byte[bufferLen]; + while ((readLen = src.Read(buffer, 0, bufferLen)) > 0) + { + dest.Write(buffer, 0, readLen); + } + + if (setDefaultPosition) + dest.Position = 0; + } + + internal static byte[] Decompress(this byte[] value, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? value.decompress() + : value; + } + + internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.decompressToArray() + : stream.ToByteArray(); + } + + /// + /// Determines whether the specified equals the specified , + /// and invokes the specified Action<int> delegate at the same time. + /// + /// + /// true if equals ; + /// otherwise, false. + /// + /// + /// An to compare. + /// + /// + /// A to compare. + /// + /// + /// An Action<int> delegate that references the method(s) called at + /// the same time as comparing. An parameter to pass to + /// the method(s) is . + /// + /// + /// isn't between 0 and 255. + /// + internal static bool EqualsWith(this int value, char c, Action action) + { + if (value < 0 || value > 255) + throw new ArgumentOutOfRangeException("value"); + + action(value); + return value == c - 0; + } + + internal static string GetMessage(this CloseStatusCode code) + { + return code == CloseStatusCode.ProtocolError + ? "A WebSocket protocol error has occurred." + : code == CloseStatusCode.IncorrectData + ? "An incorrect data has been received." + : code == CloseStatusCode.Abnormal + ? "An exception has occurred." + : code == CloseStatusCode.InconsistentData + ? "An inconsistent data has been received." + : code == CloseStatusCode.PolicyViolation + ? "A policy violation has occurred." + : code == CloseStatusCode.TooBig + ? "A too big data has been received." + : code == CloseStatusCode.IgnoreExtension + ? "WebSocket client did not receive expected extension(s)." + : code == CloseStatusCode.ServerError + ? "WebSocket server got an internal error." + : code == CloseStatusCode.TlsHandshakeFailure + ? "An error has occurred while handshaking." + : String.Empty; + } + + internal static string GetNameInternal(this string nameAndValue, string separator) + { + var i = nameAndValue.IndexOf(separator); + return i > 0 + ? nameAndValue.Substring(0, i).Trim() + : null; + } + + internal static string GetValueInternal(this string nameAndValue, string separator) + { + var i = nameAndValue.IndexOf(separator); + return i >= 0 && i < nameAndValue.Length - 1 + ? nameAndValue.Substring(i + 1).Trim() + : null; + } + + internal static bool IsCompressionExtension(this string value, CompressionMethod method) + { + return value.StartsWith(method.ToExtensionString()); + } + + internal static bool IsPortNumber(this int value) + { + return value > 0 && value < 65536; + } + + internal static bool IsReserved(this ushort code) + { + return code == (ushort)CloseStatusCode.Undefined || + code == (ushort)CloseStatusCode.NoStatusCode || + code == (ushort)CloseStatusCode.Abnormal || + code == (ushort)CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsReserved(this CloseStatusCode code) + { + return code == CloseStatusCode.Undefined || + code == CloseStatusCode.NoStatusCode || + code == CloseStatusCode.Abnormal || + code == CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsText(this string value) + { + var len = value.Length; + for (var i = 0; i < len; i++) + { + char c = value[i]; + if (c < 0x20 && !"\r\n\t".Contains(c)) + return false; + + if (c == 0x7f) + return false; + + if (c == '\n' && ++i < len) + { + c = value[i]; + if (!" \t".Contains(c)) + return false; + } + } + + return true; + } + + internal static bool IsToken(this string value) + { + foreach (char c in value) + if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c)) + return false; + + return true; + } + + internal static string Quote(this string value) + { + return value.IsToken() + ? value + : String.Format("\"{0}\"", value.Replace("\"", "\\\"")); + } + + internal static byte[] ReadBytes(this Stream stream, int length) + { + return stream.readBytes(new byte[length], 0, length); + } + + internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength) + { + using (var result = new MemoryStream()) + { + var count = length / bufferLength; + var rem = (int)(length % bufferLength); + + var buffer = new byte[bufferLength]; + var end = false; + for (long i = 0; i < count; i++) + { + if (!stream.readBytes(buffer, 0, bufferLength, result)) + { + end = true; + break; + } + } + + if (!end && rem > 0) + stream.readBytes(new byte[rem], 0, rem, result); + + return result.ToArray(); + } + } + + internal static async Task ReadBytesAsync(this Stream stream, int length) + { + var buffer = new byte[length]; + + var len = await stream.ReadAsync(buffer, 0, length).ConfigureAwait(false); + var bytes = len < 1 + ? new byte[0] + : len < length + ? stream.readBytes(buffer, len, length - len) + : buffer; + + return bytes; + } + + internal static string RemovePrefix(this string value, params string[] prefixes) + { + var i = 0; + foreach (var prefix in prefixes) + { + if (value.StartsWith(prefix)) + { + i = prefix.Length; + break; + } + } + + return i > 0 + ? value.Substring(i) + : value; + } + + internal static T[] Reverse(this T[] array) + { + var len = array.Length; + T[] reverse = new T[len]; + + var end = len - 1; + for (var i = 0; i <= end; i++) + reverse[i] = array[end - i]; + + return reverse; + } + + internal static IEnumerable SplitHeaderValue( + this string value, params char[] separator) + { + var len = value.Length; + var separators = new string(separator); + + var buffer = new StringBuilder(32); + var quoted = false; + var escaped = false; + + char c; + for (var i = 0; i < len; i++) + { + c = value[i]; + if (c == '"') + { + if (escaped) + escaped = !escaped; + else + quoted = !quoted; + } + else if (c == '\\') + { + if (i < len - 1 && value[i + 1] == '"') + escaped = true; + } + else if (separators.Contains(c)) + { + if (!quoted) + { + yield return buffer.ToString(); + buffer.Length = 0; + + continue; + } + } + else { + } + + buffer.Append(c); + } + + if (buffer.Length > 0) + yield return buffer.ToString(); + } + + internal static byte[] ToByteArray(this Stream stream) + { + using (var output = new MemoryStream()) + { + stream.Position = 0; + stream.CopyTo(output); + + return output.ToArray(); + } + } + + internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + Array.Reverse(bytes); + + return bytes; + } + + internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + Array.Reverse(bytes); + + return bytes; + } + + internal static string ToExtensionString( + this CompressionMethod method, params string[] parameters) + { + if (method == CompressionMethod.None) + return String.Empty; + + var m = String.Format("permessage-{0}", method.ToString().ToLower()); + if (parameters == null || parameters.Length == 0) + return m; + + return String.Format("{0}; {1}", m, parameters.ToString("; ")); + } + + internal static List ToList(this IEnumerable source) + { + return new List(source); + } + + internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) + { + return BitConverter.ToUInt16(src.ToHostOrder(srcOrder), 0); + } + + internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) + { + return BitConverter.ToUInt64(src.ToHostOrder(srcOrder), 0); + } + + internal static string TrimEndSlash(this string value) + { + value = value.TrimEnd('/'); + return value.Length > 0 + ? value + : "/"; + } + + internal static string Unquote(this string value) + { + var start = value.IndexOf('\"'); + var end = value.LastIndexOf('\"'); + if (start < end) + value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\""); + + return value.Trim(); + } + + internal static void WriteBytes(this Stream stream, byte[] value) + { + using (var src = new MemoryStream(value)) + { + src.CopyTo(stream); + } + } + + #endregion + + #region Public Methods + + /// + /// Determines whether the specified contains any of characters + /// in the specified array of . + /// + /// + /// true if contains any of ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains characters to find. + /// + public static bool Contains(this string value, params char[] chars) + { + return chars == null || chars.Length == 0 + ? true + : value == null || value.Length == 0 + ? false + : value.IndexOfAny(chars) != -1; + } + + /// + /// Determines whether the specified contains the entry + /// with the specified . + /// + /// + /// true if contains the entry + /// with ; otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A that represents the key of the entry to find. + /// + public static bool Contains(this QueryParamCollection collection, string name) + { + return collection == null || collection.Count == 0 + ? false + : collection[name] != null; + } + + /// + /// Determines whether the specified contains the entry + /// with the specified both and . + /// + /// + /// true if contains the entry + /// with both and ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A that represents the key of the entry to find. + /// + /// + /// A that represents the value of the entry to find. + /// + public static bool Contains(this QueryParamCollection collection, string name, string value) + { + if (collection == null || collection.Count == 0) + return false; + + var values = collection[name]; + if (values == null) + return false; + + foreach (var v in values.Split(',')) + if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + /// + /// Emits the specified delegate if it isn't . + /// + /// + /// A to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A that contains no event data. + /// + public static void Emit(this EventHandler eventHandler, object sender, EventArgs e) + { + if (eventHandler != null) + eventHandler(sender, e); + } + + /// + /// Emits the specified EventHandler<TEventArgs> delegate + /// if it isn't . + /// + /// + /// An EventHandler<TEventArgs> to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A TEventArgs that represents the event data. + /// + /// + /// The type of the event data generated by the event. + /// + public static void Emit( + this EventHandler eventHandler, object sender, TEventArgs e) + where TEventArgs : EventArgs + { + if (eventHandler != null) + eventHandler(sender, e); + } + + /// + /// Gets the collection of the HTTP cookies from the specified HTTP . + /// + /// + /// A that receives a collection of the HTTP cookies. + /// + /// + /// A that contains a collection of the HTTP headers. + /// + /// + /// true if is a collection of the response headers; + /// otherwise, false. + /// + public static CookieCollection GetCookies(this QueryParamCollection headers, bool response) + { + var name = response ? "Set-Cookie" : "Cookie"; + return headers == null || !headers.Contains(name) + ? new CookieCollection() + : CookieHelper.Parse(headers[name], response); + } + + /// + /// Gets the description of the specified HTTP status . + /// + /// + /// A that represents the description of the HTTP status code. + /// + /// + /// One of enum values, indicates the HTTP status codes. + /// + public static string GetDescription(this HttpStatusCode code) + { + return ((int)code).GetStatusDescription(); + } + + /// + /// Gets the name from the specified that contains a pair of name and + /// value separated by a separator string. + /// + /// + /// A that represents the name if any; otherwise, null. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static string GetName(this string nameAndValue, string separator) + { + return (nameAndValue != null && nameAndValue.Length > 0) && + (separator != null && separator.Length > 0) + ? nameAndValue.GetNameInternal(separator) + : null; + } + + /// + /// Gets the name and value from the specified that contains a pair of + /// name and value separated by a separator string. + /// + /// + /// A KeyValuePair<string, string> that represents the name and value if any. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static KeyValuePair GetNameAndValue( + this string nameAndValue, string separator) + { + var name = nameAndValue.GetName(separator); + var value = nameAndValue.GetValue(separator); + return name != null + ? new KeyValuePair(name, value) + : new KeyValuePair(null, null); + } + + /// + /// Gets the description of the specified HTTP status . + /// + /// + /// A that represents the description of the HTTP status code. + /// + /// + /// An that represents the HTTP status code. + /// + public static string GetStatusDescription(this int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + + return String.Empty; + } + + /// + /// Gets the value from the specified that contains a pair of name and + /// value separated by a separator string. + /// + /// + /// A that represents the value if any; otherwise, null. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static string GetValue(this string nameAndValue, string separator) + { + return (nameAndValue != null && nameAndValue.Length > 0) && + (separator != null && separator.Length > 0) + ? nameAndValue.GetValueInternal(separator) + : null; + } + + /// + /// Determines whether the specified is host + /// (this computer architecture) byte order. + /// + /// + /// true if is host byte order; + /// otherwise, false. + /// + /// + /// One of the enum values, to test. + /// + public static bool IsHostOrder(this ByteOrder order) + { + // true : !(true ^ true) or !(false ^ false) + // false: !(true ^ false) or !(false ^ true) + return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); + } + + /// + /// Determines whether the specified is a predefined scheme. + /// + /// + /// true if is a predefined scheme; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsPredefinedScheme(this string value) + { + if (value == null || value.Length < 2) + return false; + + var c = value[0]; + if (c == 'h') + return value == "http" || value == "https"; + + if (c == 'w') + return value == "ws" || value == "wss"; + + if (c == 'f') + return value == "file" || value == "ftp"; + + if (c == 'n') + { + c = value[1]; + return c == 'e' + ? value == "news" || value == "net.pipe" || value == "net.tcp" + : value == "nntp"; + } + + return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto"); + } + + /// + /// Determines whether the specified is a URI string. + /// + /// + /// true if may be a URI string; otherwise, false. + /// + /// + /// A to test. + /// + public static bool MaybeUri(this string value) + { + if (value == null || value.Length == 0) + return false; + + var i = value.IndexOf(':'); + if (i == -1) + return false; + + if (i >= 10) + return false; + + return value.Substring(0, i).IsPredefinedScheme(); + } + + /// + /// Retrieves a sub-array from the specified . + /// A sub-array starts at the specified element position. + /// + /// + /// An array of T that receives a sub-array, or an empty array of T if any problems + /// with the parameters. + /// + /// + /// An array of T that contains the data to retrieve a sub-array. + /// + /// + /// An that contains the zero-based starting position of a sub-array + /// in . + /// + /// + /// An that contains the number of elements to retrieve a sub-array. + /// + /// + /// The type of elements in the . + /// + public static T[] SubArray(this T[] array, int startIndex, int length) + { + if (array == null || array.Length == 0) + return new T[0]; + + if (startIndex < 0 || length <= 0) + return new T[0]; + + if (startIndex + length > array.Length) + return new T[0]; + + if (startIndex == 0 && array.Length == length) + return array; + + T[] subArray = new T[length]; + Array.Copy(array, startIndex, subArray, 0, length); + + return subArray; + } + + /// + /// Converts the order of the specified array of to the host byte order. + /// + /// + /// An array of converted from . + /// + /// + /// An array of to convert. + /// + /// + /// One of the enum values, indicates the byte order of + /// . + /// + /// + /// is . + /// + public static byte[] ToHostOrder(this byte[] src, ByteOrder srcOrder) + { + if (src == null) + throw new ArgumentNullException("src"); + + return src.Length > 1 && !srcOrder.IsHostOrder() + ? src.Reverse() + : src; + } + + /// + /// Converts the specified to a that + /// concatenates the each element of across the specified + /// . + /// + /// + /// A converted from , + /// or if is empty. + /// + /// + /// An array of T to convert. + /// + /// + /// A that represents the separator string. + /// + /// + /// The type of elements in . + /// + /// + /// is . + /// + public static string ToString(this T[] array, string separator) + { + if (array == null) + throw new ArgumentNullException("array"); + + var len = array.Length; + if (len == 0) + return String.Empty; + + if (separator == null) + separator = String.Empty; + + var buff = new StringBuilder(64); + (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator)); + + buff.Append(array[len - 1].ToString()); + return buff.ToString(); + } + + /// + /// Executes the specified Action<int> delegate times. + /// + /// + /// An is the number of times to execute. + /// + /// + /// An Action<int> delegate that references the method(s) to execute. + /// An parameter to pass to the method(s) is the zero-based count of + /// iteration. + /// + public static void Times(this int n, Action action) + { + if (n > 0 && action != null) + for (int i = 0; i < n; i++) + action(i); + } + + /// + /// Converts the specified to a . + /// + /// + /// A converted from , or + /// if isn't successfully converted. + /// + /// + /// A to convert. + /// + public static Uri ToUri(this string uriString) + { + Uri res; + return Uri.TryCreate( + uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out res) + ? res + : null; + } + + /// + /// URL-decodes the specified . + /// + /// + /// A that receives the decoded string, or the + /// if it's or empty. + /// + /// + /// A to decode. + /// + public static string UrlDecode(this string value) + { + return value == null || value.Length == 0 + ? value + : WebUtility.UrlDecode(value); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Fin.cs b/SocketHttpListener.Portable/Fin.cs new file mode 100644 index 000000000..f91401b99 --- /dev/null +++ b/SocketHttpListener.Portable/Fin.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Fin : byte + { + More = 0x0, + Final = 0x1 + } +} diff --git a/SocketHttpListener.Portable/HttpBase.cs b/SocketHttpListener.Portable/HttpBase.cs new file mode 100644 index 000000000..5172ba497 --- /dev/null +++ b/SocketHttpListener.Portable/HttpBase.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using System.Threading; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener +{ + internal abstract class HttpBase + { + #region Private Fields + + private QueryParamCollection _headers; + private Version _version; + + #endregion + + #region Internal Fields + + internal byte[] EntityBodyData; + + #endregion + + #region Protected Fields + + protected const string CrLf = "\r\n"; + + #endregion + + #region Protected Constructors + + protected HttpBase(Version version, QueryParamCollection headers) + { + _version = version; + _headers = headers; + } + + #endregion + + #region Public Properties + + public string EntityBody + { + get + { + var data = EntityBodyData; + + return data != null && data.Length > 0 + ? getEncoding(_headers["Content-Type"]).GetString(data, 0, data.Length) + : String.Empty; + } + } + + public QueryParamCollection Headers + { + get + { + return _headers; + } + } + + public Version ProtocolVersion + { + get + { + return _version; + } + } + + #endregion + + #region Private Methods + + private static Encoding getEncoding(string contentType) + { + if (contentType == null || contentType.Length == 0) + return Encoding.UTF8; + + var i = contentType.IndexOf("charset=", StringComparison.Ordinal); + if (i == -1) + return Encoding.UTF8; + + var charset = contentType.Substring(i + 8); + i = charset.IndexOf(';'); + if (i != -1) + charset = charset.Substring(0, i).TrimEnd(); + + return Encoding.GetEncoding(charset.Trim('"')); + } + + #endregion + + #region Public Methods + + public byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(ToString()); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/HttpResponse.cs b/SocketHttpListener.Portable/HttpResponse.cs new file mode 100644 index 000000000..5aca28c7c --- /dev/null +++ b/SocketHttpListener.Portable/HttpResponse.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Text; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; +using HttpVersion = SocketHttpListener.Net.HttpVersion; +using System.Linq; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener +{ + internal class HttpResponse : HttpBase + { + #region Private Fields + + private string _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) + : base(version, headers) + { + _code = code; + _reason = reason; + } + + #endregion + + #region Internal Constructors + + internal HttpResponse(HttpStatusCode code) + : this(code, code.GetDescription()) + { + } + + internal HttpResponse(HttpStatusCode code, string reason) + : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) + { + Headers["Server"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Public Properties + + public CookieCollection Cookies + { + get + { + return Headers.GetCookies(true); + } + } + + public bool IsProxyAuthenticationRequired + { + get + { + return _code == "407"; + } + } + + public bool IsUnauthorized + { + get + { + return _code == "401"; + } + } + + public bool IsWebSocketResponse + { + get + { + var headers = Headers; + return ProtocolVersion > HttpVersion.Version10 && + _code == "101" && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + } + } + + public string Reason + { + get + { + return _reason; + } + } + + public string StatusCode + { + get + { + return _code; + } + } + + #endregion + + #region Internal Methods + + internal static HttpResponse CreateCloseResponse(HttpStatusCode code) + { + var res = new HttpResponse(code); + res.Headers["Connection"] = "close"; + + return res; + } + + internal static HttpResponse CreateWebSocketResponse() + { + var res = new HttpResponse(HttpStatusCode.SwitchingProtocols); + + var headers = res.Headers; + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return res; + } + + #endregion + + #region Public Methods + + public void SetCookies(CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + return; + + var headers = Headers; + var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); + + foreach (var cookie in sorted) + headers.Add("Set-Cookie", cookie.ToString()); + } + + public override string ToString() + { + var output = new StringBuilder(64); + output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); + + var headers = Headers; + foreach (var key in headers.Keys) + output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); + + output.Append(CrLf); + + var entity = EntityBody; + if (entity.Length > 0) + output.Append(entity); + + return output.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Mask.cs b/SocketHttpListener.Portable/Mask.cs new file mode 100644 index 000000000..adc2f098e --- /dev/null +++ b/SocketHttpListener.Portable/Mask.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Mask : byte + { + Unmask = 0x0, + Mask = 0x1 + } +} diff --git a/SocketHttpListener.Portable/MessageEventArgs.cs b/SocketHttpListener.Portable/MessageEventArgs.cs new file mode 100644 index 000000000..9dbadb9ab --- /dev/null +++ b/SocketHttpListener.Portable/MessageEventArgs.cs @@ -0,0 +1,96 @@ +using System; +using System.Text; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the receives + /// a text or binary data frame. + /// If you want to get the received data, you access the or + /// property. + /// + public class MessageEventArgs : EventArgs + { + #region Private Fields + + private string _data; + private Opcode _opcode; + private byte[] _rawData; + + #endregion + + #region Internal Constructors + + internal MessageEventArgs (Opcode opcode, byte[] data) + { + _opcode = opcode; + _rawData = data; + _data = convertToString (opcode, data); + } + + internal MessageEventArgs (Opcode opcode, PayloadData payload) + { + _opcode = opcode; + _rawData = payload.ApplicationData; + _data = convertToString (opcode, _rawData); + } + + #endregion + + #region Public Properties + + /// + /// Gets the received data as a . + /// + /// + /// A that contains the received data. + /// + public string Data { + get { + return _data; + } + } + + /// + /// Gets the received data as an array of . + /// + /// + /// An array of that contains the received data. + /// + public byte [] RawData { + get { + return _rawData; + } + } + + /// + /// Gets the type of the received data. + /// + /// + /// One of the values, indicates the type of the received data. + /// + public Opcode Type { + get { + return _opcode; + } + } + + #endregion + + #region Private Methods + + private static string convertToString (Opcode opcode, byte [] data) + { + return data.Length == 0 + ? String.Empty + : opcode == Opcode.Text + ? Encoding.UTF8.GetString (data, 0, data.Length) + : opcode.ToString (); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs b/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs new file mode 100644 index 000000000..c6e7e538e --- /dev/null +++ b/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace SocketHttpListener.Net +{ + public delegate AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRequest httpRequest); +} diff --git a/SocketHttpListener.Portable/Net/ChunkStream.cs b/SocketHttpListener.Portable/Net/ChunkStream.cs new file mode 100644 index 000000000..3f3b4a667 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ChunkStream.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace SocketHttpListener.Net +{ + class ChunkStream + { + enum State + { + None, + PartialSize, + Body, + BodyFinished, + Trailer + } + + class Chunk + { + public byte[] Bytes; + public int Offset; + + public Chunk(byte[] chunk) + { + this.Bytes = chunk; + } + + public int Read(byte[] buffer, int offset, int size) + { + int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; + Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread); + Offset += nread; + return nread; + } + } + + internal WebHeaderCollection headers; + int chunkSize; + int chunkRead; + int totalWritten; + State state; + //byte [] waitBuffer; + StringBuilder saved; + bool sawCR; + bool gotit; + int trailerState; + List chunks; + + public ChunkStream(WebHeaderCollection headers) + { + this.headers = headers; + saved = new StringBuilder(); + chunks = new List(); + chunkSize = -1; + totalWritten = 0; + } + + public void ResetBuffer() + { + chunkSize = -1; + chunkRead = 0; + totalWritten = 0; + chunks.Clear(); + } + + public void WriteAndReadBack(byte[] buffer, int offset, int size, ref int read) + { + if (offset + read > 0) + Write(buffer, offset, offset + read); + read = Read(buffer, offset, size); + } + + public int Read(byte[] buffer, int offset, int size) + { + return ReadFromChunks(buffer, offset, size); + } + + int ReadFromChunks(byte[] buffer, int offset, int size) + { + int count = chunks.Count; + int nread = 0; + + var chunksForRemoving = new List(count); + for (int i = 0; i < count; i++) + { + Chunk chunk = (Chunk)chunks[i]; + + if (chunk.Offset == chunk.Bytes.Length) + { + chunksForRemoving.Add(chunk); + continue; + } + + nread += chunk.Read(buffer, offset + nread, size - nread); + if (nread == size) + break; + } + + foreach (var chunk in chunksForRemoving) + chunks.Remove(chunk); + + return nread; + } + + public void Write(byte[] buffer, int offset, int size) + { + if (offset < size) + InternalWrite(buffer, ref offset, size); + } + + void InternalWrite(byte[] buffer, ref int offset, int size) + { + if (state == State.None || state == State.PartialSize) + { + state = GetChunkSize(buffer, ref offset, size); + if (state == State.PartialSize) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (state == State.Body && offset < size) + { + state = ReadBody(buffer, ref offset, size); + if (state == State.Body) + return; + } + + if (state == State.BodyFinished && offset < size) + { + state = ReadCRLF(buffer, ref offset, size); + if (state == State.BodyFinished) + return; + + sawCR = false; + } + + if (state == State.Trailer && offset < size) + { + state = ReadTrailer(buffer, ref offset, size); + if (state == State.Trailer) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (offset < size) + InternalWrite(buffer, ref offset, size); + } + + public bool WantMore + { + get { return (chunkRead != chunkSize || chunkSize != 0 || state != State.None); } + } + + public bool DataAvailable + { + get + { + int count = chunks.Count; + for (int i = 0; i < count; i++) + { + Chunk ch = (Chunk)chunks[i]; + if (ch == null || ch.Bytes == null) + continue; + if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length) + return (state != State.Body); + } + return false; + } + } + + public int TotalDataSize + { + get { return totalWritten; } + } + + public int ChunkLeft + { + get { return chunkSize - chunkRead; } + } + + State ReadBody(byte[] buffer, ref int offset, int size) + { + if (chunkSize == 0) + return State.BodyFinished; + + int diff = size - offset; + if (diff + chunkRead > chunkSize) + diff = chunkSize - chunkRead; + + byte[] chunk = new byte[diff]; + Buffer.BlockCopy(buffer, offset, chunk, 0, diff); + chunks.Add(new Chunk(chunk)); + offset += diff; + chunkRead += diff; + totalWritten += diff; + return (chunkRead == chunkSize) ? State.BodyFinished : State.Body; + + } + + State GetChunkSize(byte[] buffer, ref int offset, int size) + { + chunkRead = 0; + chunkSize = 0; + char c = '\0'; + while (offset < size) + { + c = (char)buffer[offset++]; + if (c == '\r') + { + if (sawCR) + ThrowProtocolViolation("2 CR found"); + + sawCR = true; + continue; + } + + if (sawCR && c == '\n') + break; + + if (c == ' ') + gotit = true; + + if (!gotit) + saved.Append(c); + + if (saved.Length > 20) + ThrowProtocolViolation("chunk size too long."); + } + + if (!sawCR || c != '\n') + { + if (offset < size) + ThrowProtocolViolation("Missing \\n"); + + try + { + if (saved.Length > 0) + { + chunkSize = Int32.Parse(RemoveChunkExtension(saved.ToString()), NumberStyles.HexNumber); + } + } + catch (Exception) + { + ThrowProtocolViolation("Cannot parse chunk size."); + } + + return State.PartialSize; + } + + chunkRead = 0; + try + { + chunkSize = Int32.Parse(RemoveChunkExtension(saved.ToString()), NumberStyles.HexNumber); + } + catch (Exception) + { + ThrowProtocolViolation("Cannot parse chunk size."); + } + + if (chunkSize == 0) + { + trailerState = 2; + return State.Trailer; + } + + return State.Body; + } + + static string RemoveChunkExtension(string input) + { + int idx = input.IndexOf(';'); + if (idx == -1) + return input; + return input.Substring(0, idx); + } + + State ReadCRLF(byte[] buffer, ref int offset, int size) + { + if (!sawCR) + { + if ((char)buffer[offset++] != '\r') + ThrowProtocolViolation("Expecting \\r"); + + sawCR = true; + if (offset == size) + return State.BodyFinished; + } + + if (sawCR && (char)buffer[offset++] != '\n') + ThrowProtocolViolation("Expecting \\n"); + + return State.None; + } + + State ReadTrailer(byte[] buffer, ref int offset, int size) + { + char c = '\0'; + + // short path + if (trailerState == 2 && (char)buffer[offset] == '\r' && saved.Length == 0) + { + offset++; + if (offset < size && (char)buffer[offset] == '\n') + { + offset++; + return State.None; + } + offset--; + } + + int st = trailerState; + string stString = "\r\n\r"; + while (offset < size && st < 4) + { + c = (char)buffer[offset++]; + if ((st == 0 || st == 2) && c == '\r') + { + st++; + continue; + } + + if ((st == 1 || st == 3) && c == '\n') + { + st++; + continue; + } + + if (st > 0) + { + saved.Append(stString.Substring(0, saved.Length == 0 ? st - 2 : st)); + st = 0; + if (saved.Length > 4196) + ThrowProtocolViolation("Error reading trailer (too long)."); + } + } + + if (st < 4) + { + trailerState = st; + if (offset < size) + ThrowProtocolViolation("Error reading trailer."); + + return State.Trailer; + } + + StringReader reader = new StringReader(saved.ToString()); + string line; + while ((line = reader.ReadLine()) != null && line != "") + headers.Add(line); + + return State.None; + } + + static void ThrowProtocolViolation(string message) + { + WebException we = new WebException(message, null, WebExceptionStatus.UnknownError, null); + //WebException we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null); + throw we; + } + } +} diff --git a/SocketHttpListener.Portable/Net/ChunkedInputStream.cs b/SocketHttpListener.Portable/Net/ChunkedInputStream.cs new file mode 100644 index 000000000..6dfd8d8a1 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ChunkedInputStream.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + class ChunkedInputStream : RequestStream + { + bool disposed; + ChunkStream decoder; + HttpListenerContext context; + bool no_more_data; + + //class ReadBufferState + //{ + // public byte[] Buffer; + // public int Offset; + // public int Count; + // public int InitialCount; + // public HttpStreamAsyncResult Ares; + // public ReadBufferState(byte[] buffer, int offset, int count, + // HttpStreamAsyncResult ares) + // { + // Buffer = buffer; + // Offset = offset; + // Count = count; + // InitialCount = count; + // Ares = ares; + // } + //} + + public ChunkedInputStream(HttpListenerContext context, Stream stream, + byte[] buffer, int offset, int length) + : base(stream, buffer, offset, length) + { + this.context = context; + WebHeaderCollection coll = (WebHeaderCollection)context.Request.Headers; + decoder = new ChunkStream(coll); + } + + //public ChunkStream Decoder + //{ + // get { return decoder; } + // set { decoder = value; } + //} + + //public override int Read([In, Out] byte[] buffer, int offset, int count) + //{ + // IAsyncResult ares = BeginRead(buffer, offset, count, null, null); + // return EndRead(ares); + //} + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // if (buffer == null) + // throw new ArgumentNullException("buffer"); + + // int len = buffer.Length; + // if (offset < 0 || offset > len) + // throw new ArgumentOutOfRangeException("offset exceeds the size of buffer"); + + // if (count < 0 || offset > len - count) + // throw new ArgumentOutOfRangeException("offset+size exceeds the size of buffer"); + + // HttpStreamAsyncResult ares = new HttpStreamAsyncResult(); + // ares.Callback = cback; + // ares.State = state; + // if (no_more_data) + // { + // ares.Complete(); + // return ares; + // } + // int nread = decoder.Read(buffer, offset, count); + // offset += nread; + // count -= nread; + // if (count == 0) + // { + // // got all we wanted, no need to bother the decoder yet + // ares.Count = nread; + // ares.Complete(); + // return ares; + // } + // if (!decoder.WantMore) + // { + // no_more_data = nread == 0; + // ares.Count = nread; + // ares.Complete(); + // return ares; + // } + // ares.Buffer = new byte[8192]; + // ares.Offset = 0; + // ares.Count = 8192; + // ReadBufferState rb = new ReadBufferState(buffer, offset, count, ares); + // rb.InitialCount += nread; + // base.BeginRead(ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + // return ares; + //} + + //void OnRead(IAsyncResult base_ares) + //{ + // ReadBufferState rb = (ReadBufferState)base_ares.AsyncState; + // HttpStreamAsyncResult ares = rb.Ares; + // try + // { + // int nread = base.EndRead(base_ares); + // decoder.Write(ares.Buffer, ares.Offset, nread); + // nread = decoder.Read(rb.Buffer, rb.Offset, rb.Count); + // rb.Offset += nread; + // rb.Count -= nread; + // if (rb.Count == 0 || !decoder.WantMore || nread == 0) + // { + // no_more_data = !decoder.WantMore && nread == 0; + // ares.Count = rb.InitialCount - rb.Count; + // ares.Complete(); + // return; + // } + // ares.Offset = 0; + // ares.Count = Math.Min(8192, decoder.ChunkLeft + 6); + // base.BeginRead(ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + // } + // catch (Exception e) + // { + // context.Connection.SendError(e.Message, 400); + // ares.Complete(e); + // } + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // HttpStreamAsyncResult my_ares = ares as HttpStreamAsyncResult; + // if (ares == null) + // throw new ArgumentException("Invalid IAsyncResult", "ares"); + + // if (!ares.IsCompleted) + // ares.AsyncWaitHandle.WaitOne(); + + // if (my_ares.Error != null) + // throw new HttpListenerException(400, "I/O operation aborted: " + my_ares.Error.Message); + + // return my_ares.Count; + //} + + //protected override void Dispose(bool disposing) + //{ + // if (!disposed) + // { + // disposed = true; + // base.Dispose(disposing); + // } + //} + } +} diff --git a/SocketHttpListener.Portable/Net/CookieHelper.cs b/SocketHttpListener.Portable/Net/CookieHelper.cs new file mode 100644 index 000000000..470507d6b --- /dev/null +++ b/SocketHttpListener.Portable/Net/CookieHelper.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + public static class CookieHelper + { + internal static CookieCollection Parse(string value, bool response) + { + return response + ? parseResponse(value) + : null; + } + + private static string[] splitCookieHeaderValue(string value) + { + return new List(value.SplitHeaderValue(',', ';')).ToArray(); + } + + private static CookieCollection parseResponse(string value) + { + var cookies = new CookieCollection(); + + Cookie cookie = null; + var pairs = splitCookieHeaderValue(value); + for (int i = 0; i < pairs.Length; i++) + { + var pair = pairs[i].Trim(); + if (pair.Length == 0) + continue; + + if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Version = Int32.Parse(pair.GetValueInternal("=").Trim('"')); + } + else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase)) + { + var buffer = new StringBuilder(pair.GetValueInternal("="), 32); + if (i < pairs.Length - 1) + buffer.AppendFormat(", {0}", pairs[++i].Trim()); + + DateTime expires; + if (!DateTime.TryParseExact( + buffer.ToString(), + new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, + new CultureInfo("en-US"), + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, + out expires)) + expires = DateTime.Now; + + if (cookie != null && cookie.Expires == DateTime.MinValue) + cookie.Expires = expires.ToLocalTime(); + } + else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase)) + { + var max = Int32.Parse(pair.GetValueInternal("=").Trim('"')); + var expires = DateTime.Now.AddSeconds((double)max); + if (cookie != null) + cookie.Expires = expires; + } + else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Path = pair.GetValueInternal("="); + } + else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Domain = pair.GetValueInternal("="); + } + else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase)) + { + var port = pair.Equals("port", StringComparison.OrdinalIgnoreCase) + ? "\"\"" + : pair.GetValueInternal("="); + + if (cookie != null) + cookie.Port = port; + } + else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Comment = pair.GetValueInternal("=").UrlDecode(); + } + else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.CommentUri = pair.GetValueInternal("=").Trim('"').ToUri(); + } + else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Discard = true; + } + else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Secure = true; + } + else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.HttpOnly = true; + } + else + { + if (cookie != null) + cookies.Add(cookie); + + string name; + string val = String.Empty; + + var pos = pair.IndexOf('='); + if (pos == -1) + { + name = pair; + } + else if (pos == pair.Length - 1) + { + name = pair.Substring(0, pos).TrimEnd(' '); + } + else + { + name = pair.Substring(0, pos).TrimEnd(' '); + val = pair.Substring(pos + 1).TrimStart(' '); + } + + cookie = new Cookie(name, val); + } + } + + if (cookie != null) + cookies.Add(cookie); + + return cookies; + } + } +} diff --git a/SocketHttpListener.Portable/Net/EndPointListener.cs b/SocketHttpListener.Portable/Net/EndPointListener.cs new file mode 100644 index 000000000..b50660ad0 --- /dev/null +++ b/SocketHttpListener.Portable/Net/EndPointListener.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class EndPointListener + { + HttpListener listener; + IpEndPointInfo endpoint; + ISocket sock; + Dictionary prefixes; // Dictionary + List unhandled; // List unhandled; host = '*' + List all; // List all; host = '+' + ICertificate cert; + bool secure; + Dictionary unregistered; + private readonly ILogger _logger; + private bool _closed; + private readonly bool _enableDualMode; + private readonly ICryptoProvider _cryptoProvider; + private readonly IStreamFactory _streamFactory; + private readonly ISocketFactory _socketFactory; + private readonly ITextEncoding _textEncoding; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + public EndPointListener(HttpListener listener, IpAddressInfo addr, int port, bool secure, ICertificate cert, ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.listener = listener; + _logger = logger; + _cryptoProvider = cryptoProvider; + _streamFactory = streamFactory; + _socketFactory = socketFactory; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + + this.secure = secure; + this.cert = cert; + + _enableDualMode = addr.Equals(IpAddressInfo.IPv6Any); + endpoint = new IpEndPointInfo(addr, port); + + prefixes = new Dictionary(); + unregistered = new Dictionary(); + + CreateSocket(); + } + + internal HttpListener Listener + { + get + { + return listener; + } + } + + private void CreateSocket() + { + sock = _socketFactory.CreateSocket(endpoint.IpAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp, _enableDualMode); + + sock.Bind(endpoint); + + // This is the number TcpListener uses. + sock.Listen(2147483647); + + sock.StartAccept(ProcessAccept, () => _closed); + _closed = false; + } + + private async void ProcessAccept(ISocket accepted) + { + try + { + var listener = this; + + if (listener.secure && listener.cert == null) + { + accepted.Close(); + return; + } + + HttpConnection conn = await HttpConnection.Create(_logger, accepted, listener, listener.secure, listener.cert, _cryptoProvider, _streamFactory, _memoryStreamFactory, _textEncoding).ConfigureAwait(false); + + //_logger.Debug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId); + lock (listener.unregistered) + { + listener.unregistered[conn] = conn; + } + conn.BeginReadRequest(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ProcessAccept", ex); + } + } + + internal void RemoveConnection(HttpConnection conn) + { + lock (unregistered) + { + unregistered.Remove(conn); + } + } + + public bool BindContext(HttpListenerContext context) + { + HttpListenerRequest req = context.Request; + ListenerPrefix prefix; + HttpListener listener = SearchListener(req.Url, out prefix); + if (listener == null) + return false; + + context.Listener = listener; + context.Connection.Prefix = prefix; + return true; + } + + public void UnbindContext(HttpListenerContext context) + { + if (context == null || context.Request == null) + return; + + context.Listener.UnregisterContext(context); + } + + HttpListener SearchListener(Uri uri, out ListenerPrefix prefix) + { + prefix = null; + if (uri == null) + return null; + + string host = uri.Host; + int port = uri.Port; + string path = WebUtility.UrlDecode(uri.AbsolutePath); + string path_slash = path[path.Length - 1] == '/' ? path : path + "/"; + + HttpListener best_match = null; + int best_length = -1; + + if (host != null && host != "") + { + var p_ro = prefixes; + foreach (ListenerPrefix p in p_ro.Keys) + { + string ppath = p.Path; + if (ppath.Length < best_length) + continue; + + if (p.Host != host || p.Port != port) + continue; + + if (path.StartsWith(ppath) || path_slash.StartsWith(ppath)) + { + best_length = ppath.Length; + best_match = (HttpListener)p_ro[p]; + prefix = p; + } + } + if (best_length != -1) + return best_match; + } + + List list = unhandled; + best_match = MatchFromList(host, path, list, out prefix); + if (path != path_slash && best_match == null) + best_match = MatchFromList(host, path_slash, list, out prefix); + if (best_match != null) + return best_match; + + list = all; + best_match = MatchFromList(host, path, list, out prefix); + if (path != path_slash && best_match == null) + best_match = MatchFromList(host, path_slash, list, out prefix); + if (best_match != null) + return best_match; + + return null; + } + + HttpListener MatchFromList(string host, string path, List list, out ListenerPrefix prefix) + { + prefix = null; + if (list == null) + return null; + + HttpListener best_match = null; + int best_length = -1; + + foreach (ListenerPrefix p in list) + { + string ppath = p.Path; + if (ppath.Length < best_length) + continue; + + if (path.StartsWith(ppath)) + { + best_length = ppath.Length; + best_match = p.Listener; + prefix = p; + } + } + + return best_match; + } + + void AddSpecial(List coll, ListenerPrefix prefix) + { + if (coll == null) + return; + + foreach (ListenerPrefix p in coll) + { + if (p.Path == prefix.Path) //TODO: code + throw new HttpListenerException(400, "Prefix already in use."); + } + coll.Add(prefix); + } + + bool RemoveSpecial(List coll, ListenerPrefix prefix) + { + if (coll == null) + return false; + + int c = coll.Count; + for (int i = 0; i < c; i++) + { + ListenerPrefix p = (ListenerPrefix)coll[i]; + if (p.Path == prefix.Path) + { + coll.RemoveAt(i); + return true; + } + } + return false; + } + + void CheckIfRemove() + { + if (prefixes.Count > 0) + return; + + List list = unhandled; + if (list != null && list.Count > 0) + return; + + list = all; + if (list != null && list.Count > 0) + return; + + EndPointManager.RemoveEndPoint(this, endpoint); + } + + public void Close() + { + _closed = true; + sock.Close(); + lock (unregistered) + { + // + // Clone the list because RemoveConnection can be called from Close + // + var connections = new List(unregistered.Keys); + + foreach (HttpConnection c in connections) + c.Close(true); + unregistered.Clear(); + } + } + + public void AddPrefix(ListenerPrefix prefix, HttpListener listener) + { + List current; + List future; + if (prefix.Host == "*") + { + do + { + current = unhandled; + future = (current != null) ? current.ToList() : new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } while (Interlocked.CompareExchange(ref unhandled, future, current) != current); + return; + } + + if (prefix.Host == "+") + { + do + { + current = all; + future = (current != null) ? current.ToList() : new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } while (Interlocked.CompareExchange(ref all, future, current) != current); + return; + } + + Dictionary prefs; + Dictionary p2; + do + { + prefs = prefixes; + if (prefs.ContainsKey(prefix)) + { + HttpListener other = (HttpListener)prefs[prefix]; + if (other != listener) // TODO: code. + throw new HttpListenerException(400, "There's another listener for " + prefix); + return; + } + p2 = new Dictionary(prefs); + p2[prefix] = listener; + } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs); + } + + public void RemovePrefix(ListenerPrefix prefix, HttpListener listener) + { + List current; + List future; + if (prefix.Host == "*") + { + do + { + current = unhandled; + future = (current != null) ? current.ToList() : new List(); + if (!RemoveSpecial(future, prefix)) + break; // Prefix not found + } while (Interlocked.CompareExchange(ref unhandled, future, current) != current); + CheckIfRemove(); + return; + } + + if (prefix.Host == "+") + { + do + { + current = all; + future = (current != null) ? current.ToList() : new List(); + if (!RemoveSpecial(future, prefix)) + break; // Prefix not found + } while (Interlocked.CompareExchange(ref all, future, current) != current); + CheckIfRemove(); + return; + } + + Dictionary prefs; + Dictionary p2; + do + { + prefs = prefixes; + if (!prefs.ContainsKey(prefix)) + break; + + p2 = new Dictionary(prefs); + p2.Remove(prefix); + } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs); + CheckIfRemove(); + } + } +} diff --git a/SocketHttpListener.Portable/Net/EndPointManager.cs b/SocketHttpListener.Portable/Net/EndPointManager.cs new file mode 100644 index 000000000..797684b3e --- /dev/null +++ b/SocketHttpListener.Portable/Net/EndPointManager.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class EndPointManager + { + // Dictionary> + static Dictionary> ip_to_endpoints = new Dictionary>(); + + private EndPointManager() + { + } + + public static void AddListener(ILogger logger, HttpListener listener) + { + List added = new List(); + try + { + lock (ip_to_endpoints) + { + foreach (string prefix in listener.Prefixes) + { + AddPrefixInternal(logger, prefix, listener); + added.Add(prefix); + } + } + } + catch + { + foreach (string prefix in added) + { + RemovePrefix(logger, prefix, listener); + } + throw; + } + } + + public static void AddPrefix(ILogger logger, string prefix, HttpListener listener) + { + lock (ip_to_endpoints) + { + AddPrefixInternal(logger, prefix, listener); + } + } + + static void AddPrefixInternal(ILogger logger, string p, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix(p); + if (lp.Path.IndexOf('%') != -1) + throw new HttpListenerException(400, "Invalid path."); + + if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) // TODO: Code? + throw new HttpListenerException(400, "Invalid path."); + + // listens on all the interfaces if host name cannot be parsed by IPAddress. + EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result; + epl.AddPrefix(lp, listener); + } + + private static IpAddressInfo GetIpAnyAddress(HttpListener listener) + { + return listener.EnableDualMode ? IpAddressInfo.IPv6Any : IpAddressInfo.Any; + } + + static async Task GetEPListener(ILogger logger, string host, int port, HttpListener listener, bool secure) + { + var networkManager = listener.NetworkManager; + + IpAddressInfo addr; + if (host == "*" || host == "+") + addr = GetIpAnyAddress(listener); + else if (networkManager.TryParseIpAddress(host, out addr) == false) + { + try + { + addr = (await networkManager.GetHostAddressesAsync(host).ConfigureAwait(false)).FirstOrDefault() ?? + GetIpAnyAddress(listener); + } + catch + { + addr = GetIpAnyAddress(listener); + } + } + + Dictionary p = null; // Dictionary + if (!ip_to_endpoints.TryGetValue(addr.Address, out p)) + { + p = new Dictionary(); + ip_to_endpoints[addr.Address] = p; + } + + EndPointListener epl = null; + if (p.ContainsKey(port)) + { + epl = (EndPointListener)p[port]; + } + else + { + epl = new EndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.StreamFactory, listener.SocketFactory, listener.MemoryStreamFactory, listener.TextEncoding); + p[port] = epl; + } + + return epl; + } + + public static void RemoveEndPoint(EndPointListener epl, IpEndPointInfo ep) + { + lock (ip_to_endpoints) + { + // Dictionary p + Dictionary p; + if (ip_to_endpoints.TryGetValue(ep.IpAddress.Address, out p)) + { + p.Remove(ep.Port); + if (p.Count == 0) + { + ip_to_endpoints.Remove(ep.IpAddress.Address); + } + } + epl.Close(); + } + } + + public static void RemoveListener(ILogger logger, HttpListener listener) + { + lock (ip_to_endpoints) + { + foreach (string prefix in listener.Prefixes) + { + RemovePrefixInternal(logger, prefix, listener); + } + } + } + + public static void RemovePrefix(ILogger logger, string prefix, HttpListener listener) + { + lock (ip_to_endpoints) + { + RemovePrefixInternal(logger, prefix, listener); + } + } + + static void RemovePrefixInternal(ILogger logger, string prefix, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix(prefix); + if (lp.Path.IndexOf('%') != -1) + return; + + if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) + return; + + EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result; + epl.RemovePrefix(lp, listener); + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs new file mode 100644 index 000000000..d31da4132 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -0,0 +1,550 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class HttpConnection + { + const int BufferSize = 8192; + ISocket sock; + Stream stream; + EndPointListener epl; + MemoryStream ms; + byte[] buffer; + HttpListenerContext context; + StringBuilder current_line; + ListenerPrefix prefix; + RequestStream i_stream; + ResponseStream o_stream; + bool chunked; + int reuses; + bool context_bound; + bool secure; + int s_timeout = 300000; // 90k ms for first request, 15k ms from then on + IpEndPointInfo local_ep; + HttpListener last_listener; + int[] client_cert_errors; + ICertificate cert; + Stream ssl_stream; + + private ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + private readonly IStreamFactory _streamFactory; + + private HttpConnection(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + _logger = logger; + this.sock = sock; + this.epl = epl; + this.secure = secure; + this.cert = cert; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + _streamFactory = streamFactory; + } + + private async Task InitStream() + { + if (secure == false) + { + stream = _streamFactory.CreateNetworkStream(sock, false); + } + else + { + //ssl_stream = epl.Listener.CreateSslStream(new NetworkStream(sock, false), false, (t, c, ch, e) => + //{ + // if (c == null) + // return true; + // var c2 = c as X509Certificate2; + // if (c2 == null) + // c2 = new X509Certificate2(c.GetRawCertData()); + // client_cert = c2; + // client_cert_errors = new int[] { (int)e }; + // return true; + //}); + //stream = ssl_stream.AuthenticatedStream; + + ssl_stream = _streamFactory.CreateSslStream(_streamFactory.CreateNetworkStream(sock, false), false); + await _streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert).ConfigureAwait(false); + stream = ssl_stream; + } + Init(); + } + + public static async Task Create(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding); + + await connection.InitStream().ConfigureAwait(false); + + return connection; + } + + public Stream Stream + { + get + { + return stream; + } + } + + internal int[] ClientCertificateErrors + { + get { return client_cert_errors; } + } + + void Init() + { + if (ssl_stream != null) + { + //ssl_stream.AuthenticateAsServer(client_cert, true, (SslProtocols)ServicePointManager.SecurityProtocol, false); + //_streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert); + } + + context_bound = false; + i_stream = null; + o_stream = null; + prefix = null; + chunked = false; + ms = _memoryStreamFactory.CreateNew(); + position = 0; + input_state = InputState.RequestLine; + line_state = LineState.None; + context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding); + } + + public bool IsClosed + { + get { return (sock == null); } + } + + public int Reuses + { + get { return reuses; } + } + + public IpEndPointInfo LocalEndPoint + { + get + { + if (local_ep != null) + return local_ep; + + local_ep = (IpEndPointInfo)sock.LocalEndPoint; + return local_ep; + } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return (IpEndPointInfo)sock.RemoteEndPoint; } + } + + public bool IsSecure + { + get { return secure; } + } + + public ListenerPrefix Prefix + { + get { return prefix; } + set { prefix = value; } + } + + public async Task BeginReadRequest() + { + if (buffer == null) + buffer = new byte[BufferSize]; + + try + { + //if (reuses == 1) + // s_timeout = 15000; + var nRead = await stream.ReadAsync(buffer, 0, BufferSize).ConfigureAwait(false); + + OnReadInternal(nRead); + } + catch (Exception ex) + { + OnReadInternalException(ms, ex); + } + } + + public RequestStream GetRequestStream(bool chunked, long contentlength) + { + if (i_stream == null) + { + byte[] buffer; + _memoryStreamFactory.TryGetBuffer(ms, out buffer); + + int length = (int)ms.Length; + ms = null; + if (chunked) + { + this.chunked = true; + //context.Response.SendChunked = true; + i_stream = new ChunkedInputStream(context, stream, buffer, position, length - position); + } + else + { + i_stream = new RequestStream(stream, buffer, position, length - position, contentlength); + } + } + return i_stream; + } + + public ResponseStream GetResponseStream() + { + // TODO: can we get this stream before reading the input? + if (o_stream == null) + { + HttpListener listener = context.Listener; + + if (listener == null) + return new ResponseStream(stream, context.Response, true, _memoryStreamFactory, _textEncoding); + + o_stream = new ResponseStream(stream, context.Response, listener.IgnoreWriteExceptions, _memoryStreamFactory, _textEncoding); + } + return o_stream; + } + + void OnReadInternal(int nread) + { + ms.Write(buffer, 0, nread); + if (ms.Length > 32768) + { + SendError("Bad request", 400); + Close(true); + return; + } + + if (nread == 0) + { + //if (ms.Length > 0) + // SendError (); // Why bother? + CloseSocket(); + Unbind(); + return; + } + + if (ProcessInput(ms)) + { + if (!context.HaveError) + context.Request.FinishInitialization(); + + if (context.HaveError) + { + SendError(); + Close(true); + return; + } + + if (!epl.BindContext(context)) + { + SendError("Invalid host", 400); + Close(true); + return; + } + HttpListener listener = context.Listener; + if (last_listener != listener) + { + RemoveConnection(); + listener.AddConnection(this); + last_listener = listener; + } + + context_bound = true; + listener.RegisterContext(context); + return; + } + + BeginReadRequest(); + } + + private void OnReadInternalException(MemoryStream ms, Exception ex) + { + //_logger.ErrorException("Error in HttpConnection.OnReadInternal", ex); + + if (ms != null && ms.Length > 0) + SendError(); + if (sock != null) + { + CloseSocket(); + Unbind(); + } + } + + void RemoveConnection() + { + if (last_listener == null) + epl.RemoveConnection(this); + else + last_listener.RemoveConnection(this); + } + + enum InputState + { + RequestLine, + Headers + } + + enum LineState + { + None, + CR, + LF + } + + InputState input_state = InputState.RequestLine; + LineState line_state = LineState.None; + int position; + + // true -> done processing + // false -> need more input + bool ProcessInput(MemoryStream ms) + { + byte[] buffer; + _memoryStreamFactory.TryGetBuffer(ms, out buffer); + + int len = (int)ms.Length; + int used = 0; + string line; + + while (true) + { + if (context.HaveError) + return true; + + if (position >= len) + break; + + try + { + line = ReadLine(buffer, position, len - position, ref used); + position += used; + } + catch + { + context.ErrorMessage = "Bad request"; + context.ErrorStatus = 400; + return true; + } + + if (line == null) + break; + + if (line == "") + { + if (input_state == InputState.RequestLine) + continue; + current_line = null; + ms = null; + return true; + } + + if (input_state == InputState.RequestLine) + { + context.Request.SetRequestLine(line); + input_state = InputState.Headers; + } + else + { + try + { + context.Request.AddHeader(line); + } + catch (Exception e) + { + context.ErrorMessage = e.Message; + context.ErrorStatus = 400; + return true; + } + } + } + + if (used == len) + { + ms.SetLength(0); + position = 0; + } + return false; + } + + string ReadLine(byte[] buffer, int offset, int len, ref int used) + { + if (current_line == null) + current_line = new StringBuilder(128); + int last = offset + len; + used = 0; + + for (int i = offset; i < last && line_state != LineState.LF; i++) + { + used++; + byte b = buffer[i]; + if (b == 13) + { + line_state = LineState.CR; + } + else if (b == 10) + { + line_state = LineState.LF; + } + else + { + current_line.Append((char)b); + } + } + + string result = null; + if (line_state == LineState.LF) + { + line_state = LineState.None; + result = current_line.ToString(); + current_line.Length = 0; + } + + return result; + } + + public void SendError(string msg, int status) + { + try + { + HttpListenerResponse response = context.Response; + response.StatusCode = status; + response.ContentType = "text/html"; + string description = HttpListenerResponse.GetStatusDescription(status); + string str; + if (msg != null) + str = String.Format("

{0} ({1})

", description, msg); + else + str = String.Format("

{0}

", description); + + byte[] error = context.Response.ContentEncoding.GetBytes(str); + response.Close(error, false); + } + catch + { + // response was already closed + } + } + + public void SendError() + { + SendError(context.ErrorMessage, context.ErrorStatus); + } + + void Unbind() + { + if (context_bound) + { + epl.UnbindContext(context); + context_bound = false; + } + } + + public void Close() + { + Close(false); + } + + private void CloseSocket() + { + if (sock == null) + return; + + try + { + sock.Close(); + } + catch + { + } + finally + { + sock = null; + } + RemoveConnection(); + } + + internal void Close(bool force_close) + { + if (sock != null) + { + if (!context.Request.IsWebSocketRequest || force_close) + { + Stream st = GetResponseStream(); + if (st != null) + st.Dispose(); + + o_stream = null; + } + } + + if (sock != null) + { + force_close |= !context.Request.KeepAlive; + if (!force_close) + force_close = (context.Response.Headers["connection"] == "close"); + /* + if (!force_close) { +// bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || +// status_code == 413 || status_code == 414 || status_code == 500 || +// status_code == 503); + force_close |= (context.Request.ProtocolVersion <= HttpVersion.Version10); + } + */ + + if (!force_close && context.Request.FlushInput()) + { + if (chunked && context.Response.ForceCloseChunked == false) + { + // Don't close. Keep working. + reuses++; + Unbind(); + Init(); + BeginReadRequest(); + return; + } + + reuses++; + Unbind(); + Init(); + BeginReadRequest(); + return; + } + + ISocket s = sock; + sock = null; + try + { + if (s != null) + s.Shutdown(true); + } + catch + { + } + finally + { + if (s != null) + s.Close(); + } + Unbind(); + RemoveConnection(); + return; + } + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/HttpListener.cs b/SocketHttpListener.Portable/Net/HttpListener.cs new file mode 100644 index 000000000..83660100a --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListener.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListener : IDisposable + { + internal ICryptoProvider CryptoProvider { get; private set; } + internal IStreamFactory StreamFactory { get; private set; } + internal ISocketFactory SocketFactory { get; private set; } + internal ITextEncoding TextEncoding { get; private set; } + internal IMemoryStreamFactory MemoryStreamFactory { get; private set; } + internal INetworkManager NetworkManager { get; private set; } + + public bool EnableDualMode { get; set; } + + AuthenticationSchemes auth_schemes; + HttpListenerPrefixCollection prefixes; + AuthenticationSchemeSelector auth_selector; + string realm; + bool ignore_write_exceptions; + bool unsafe_ntlm_auth; + bool listening; + bool disposed; + + Dictionary registry; // Dictionary + Dictionary connections; + private ILogger _logger; + private ICertificate _certificate; + + public Action OnContext { get; set; } + + public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + { + _logger = logger; + CryptoProvider = cryptoProvider; + StreamFactory = streamFactory; + SocketFactory = socketFactory; + NetworkManager = networkManager; + TextEncoding = textEncoding; + MemoryStreamFactory = memoryStreamFactory; + prefixes = new HttpListenerPrefixCollection(logger, this); + registry = new Dictionary(); + connections = new Dictionary(); + auth_schemes = AuthenticationSchemes.Anonymous; + } + + public HttpListener(ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + :this(new NullLogger(), certificate, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + { + } + + public HttpListener(ILogger logger, ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + : this(logger, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + { + _certificate = certificate; + } + + public void LoadCert(ICertificate cert) + { + _certificate = cert; + } + + // TODO: Digest, NTLM and Negotiate require ControlPrincipal + public AuthenticationSchemes AuthenticationSchemes + { + get { return auth_schemes; } + set + { + CheckDisposed(); + auth_schemes = value; + } + } + + public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate + { + get { return auth_selector; } + set + { + CheckDisposed(); + auth_selector = value; + } + } + + public bool IgnoreWriteExceptions + { + get { return ignore_write_exceptions; } + set + { + CheckDisposed(); + ignore_write_exceptions = value; + } + } + + public bool IsListening + { + get { return listening; } + } + + public static bool IsSupported + { + get { return true; } + } + + public HttpListenerPrefixCollection Prefixes + { + get + { + CheckDisposed(); + return prefixes; + } + } + + // TODO: use this + public string Realm + { + get { return realm; } + set + { + CheckDisposed(); + realm = value; + } + } + + public bool UnsafeConnectionNtlmAuthentication + { + get { return unsafe_ntlm_auth; } + set + { + CheckDisposed(); + unsafe_ntlm_auth = value; + } + } + + //internal IMonoSslStream CreateSslStream(Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback) + //{ + // lock (registry) + // { + // if (tlsProvider == null) + // tlsProvider = MonoTlsProviderFactory.GetProviderInternal(); + // if (tlsSettings == null) + // tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings(); + // if (tlsSettings.RemoteCertificateValidationCallback == null) + // tlsSettings.RemoteCertificateValidationCallback = callback; + // return tlsProvider.CreateSslStream(innerStream, ownsStream, tlsSettings); + // } + //} + + internal ICertificate Certificate + { + get { return _certificate; } + } + + public void Abort() + { + if (disposed) + return; + + if (!listening) + { + return; + } + + Close(true); + } + + public void Close() + { + if (disposed) + return; + + if (!listening) + { + disposed = true; + return; + } + + Close(true); + disposed = true; + } + + void Close(bool force) + { + CheckDisposed(); + EndPointManager.RemoveListener(_logger, this); + Cleanup(force); + } + + void Cleanup(bool close_existing) + { + lock (registry) + { + if (close_existing) + { + // Need to copy this since closing will call UnregisterContext + ICollection keys = registry.Keys; + var all = new HttpListenerContext[keys.Count]; + keys.CopyTo(all, 0); + registry.Clear(); + for (int i = all.Length - 1; i >= 0; i--) + all[i].Connection.Close(true); + } + + lock (connections) + { + ICollection keys = connections.Keys; + var conns = new HttpConnection[keys.Count]; + keys.CopyTo(conns, 0); + connections.Clear(); + for (int i = conns.Length - 1; i >= 0; i--) + conns[i].Close(true); + } + } + } + + internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerContext context) + { + if (AuthenticationSchemeSelectorDelegate != null) + return AuthenticationSchemeSelectorDelegate(context.Request); + else + return auth_schemes; + } + + public void Start() + { + CheckDisposed(); + if (listening) + return; + + EndPointManager.AddListener(_logger, this); + listening = true; + } + + public void Stop() + { + CheckDisposed(); + listening = false; + Close(false); + } + + void IDisposable.Dispose() + { + if (disposed) + return; + + Close(true); //TODO: Should we force here or not? + disposed = true; + } + + internal void CheckDisposed() + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + } + + internal void RegisterContext(HttpListenerContext context) + { + if (OnContext != null && IsListening) + { + OnContext(context); + } + + lock (registry) + registry[context] = context; + } + + internal void UnregisterContext(HttpListenerContext context) + { + lock (registry) + registry.Remove(context); + } + + internal void AddConnection(HttpConnection cnc) + { + lock (connections) + { + connections[cnc] = cnc; + } + } + + internal void RemoveConnection(HttpConnection cnc) + { + lock (connections) + { + connections.Remove(cnc); + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs b/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs new file mode 100644 index 000000000..faa26693d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs @@ -0,0 +1,70 @@ +using System.Security.Principal; + +namespace SocketHttpListener.Net +{ + public class HttpListenerBasicIdentity : GenericIdentity + { + string password; + + public HttpListenerBasicIdentity(string username, string password) + : base(username, "Basic") + { + this.password = password; + } + + public virtual string Password + { + get { return password; } + } + } + + public class GenericIdentity : IIdentity + { + private string m_name; + private string m_type; + + public GenericIdentity(string name) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + m_name = name; + m_type = ""; + } + + public GenericIdentity(string name, string type) + { + if (name == null) + throw new System.ArgumentNullException("name"); + if (type == null) + throw new System.ArgumentNullException("type"); + + m_name = name; + m_type = type; + } + + public virtual string Name + { + get + { + return m_name; + } + } + + public virtual string AuthenticationType + { + get + { + return m_type; + } + } + + public virtual bool IsAuthenticated + { + get + { + return !m_name.Equals(""); + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerContext.cs b/SocketHttpListener.Portable/Net/HttpListenerContext.cs new file mode 100644 index 000000000..84c6a8c19 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerContext.cs @@ -0,0 +1,201 @@ +using System; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Text; +using SocketHttpListener.Net.WebSockets; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerContext + { + HttpListenerRequest request; + HttpListenerResponse response; + IPrincipal user; + HttpConnection cnc; + string error; + int err_status = 400; + internal HttpListener Listener; + private readonly ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + + internal HttpListenerContext(HttpConnection cnc, ILogger logger, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.cnc = cnc; + _logger = logger; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + request = new HttpListenerRequest(this, _textEncoding); + response = new HttpListenerResponse(this, _logger, _textEncoding); + } + + internal int ErrorStatus + { + get { return err_status; } + set { err_status = value; } + } + + internal string ErrorMessage + { + get { return error; } + set { error = value; } + } + + internal bool HaveError + { + get { return (error != null); } + } + + internal HttpConnection Connection + { + get { return cnc; } + } + + public HttpListenerRequest Request + { + get { return request; } + } + + public HttpListenerResponse Response + { + get { return response; } + } + + public IPrincipal User + { + get { return user; } + } + + internal void ParseAuthentication(AuthenticationSchemes expectedSchemes) + { + if (expectedSchemes == AuthenticationSchemes.Anonymous) + return; + + // TODO: Handle NTLM/Digest modes + string header = request.Headers["Authorization"]; + if (header == null || header.Length < 2) + return; + + string[] authenticationData = header.Split(new char[] { ' ' }, 2); + if (string.Equals(authenticationData[0], "basic", StringComparison.OrdinalIgnoreCase)) + { + user = ParseBasicAuthentication(authenticationData[1]); + } + // TODO: throw if malformed -> 400 bad request + } + + internal IPrincipal ParseBasicAuthentication(string authData) + { + try + { + // Basic AUTH Data is a formatted Base64 String + //string domain = null; + string user = null; + string password = null; + int pos = -1; + var authDataBytes = Convert.FromBase64String(authData); + string authString = _textEncoding.GetDefaultEncoding().GetString(authDataBytes, 0, authDataBytes.Length); + + // The format is DOMAIN\username:password + // Domain is optional + + pos = authString.IndexOf(':'); + + // parse the password off the end + password = authString.Substring(pos + 1); + + // discard the password + authString = authString.Substring(0, pos); + + // check if there is a domain + pos = authString.IndexOf('\\'); + + if (pos > 0) + { + //domain = authString.Substring (0, pos); + user = authString.Substring(pos); + } + else + { + user = authString; + } + + HttpListenerBasicIdentity identity = new HttpListenerBasicIdentity(user, password); + // TODO: What are the roles MS sets + return new GenericPrincipal(identity, new string[0]); + } + catch (Exception) + { + // Invalid auth data is swallowed silently + return null; + } + } + + public HttpListenerWebSocketContext AcceptWebSocket(string protocol) + { + if (protocol != null) + { + if (protocol.Length == 0) + throw new ArgumentException("An empty string.", "protocol"); + + if (!protocol.IsToken()) + throw new ArgumentException("Contains an invalid character.", "protocol"); + } + + return new HttpListenerWebSocketContext(this, protocol, _cryptoProvider, _memoryStreamFactory); + } + } + + public class GenericPrincipal : IPrincipal + { + private IIdentity m_identity; + private string[] m_roles; + + public GenericPrincipal(IIdentity identity, string[] roles) + { + if (identity == null) + throw new ArgumentNullException("identity"); + + m_identity = identity; + if (roles != null) + { + m_roles = new string[roles.Length]; + for (int i = 0; i < roles.Length; ++i) + { + m_roles[i] = roles[i]; + } + } + else + { + m_roles = null; + } + } + + public virtual IIdentity Identity + { + get + { + return m_identity; + } + } + + public virtual bool IsInRole(string role) + { + if (role == null || m_roles == null) + return false; + + for (int i = 0; i < m_roles.Length; ++i) + { + if (m_roles[i] != null && String.Compare(m_roles[i], role, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + return false; + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs new file mode 100644 index 000000000..0b05539ee --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using MediaBrowser.Model.Logging; + +namespace SocketHttpListener.Net +{ + public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable + { + List prefixes = new List(); + HttpListener listener; + + private ILogger _logger; + + internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener) + { + _logger = logger; + this.listener = listener; + } + + public int Count + { + get { return prefixes.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool IsSynchronized + { + get { return false; } + } + + public void Add(string uriPrefix) + { + listener.CheckDisposed(); + ListenerPrefix.CheckUri(uriPrefix); + if (prefixes.Contains(uriPrefix)) + return; + + prefixes.Add(uriPrefix); + if (listener.IsListening) + EndPointManager.AddPrefix(_logger, uriPrefix, listener); + } + + public void Clear() + { + listener.CheckDisposed(); + prefixes.Clear(); + if (listener.IsListening) + EndPointManager.RemoveListener(_logger, listener); + } + + public bool Contains(string uriPrefix) + { + listener.CheckDisposed(); + return prefixes.Contains(uriPrefix); + } + + public void CopyTo(string[] array, int offset) + { + listener.CheckDisposed(); + prefixes.CopyTo(array, offset); + } + + public void CopyTo(Array array, int offset) + { + listener.CheckDisposed(); + ((ICollection)prefixes).CopyTo(array, offset); + } + + public IEnumerator GetEnumerator() + { + return prefixes.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return prefixes.GetEnumerator(); + } + + public bool Remove(string uriPrefix) + { + listener.CheckDisposed(); + if (uriPrefix == null) + throw new ArgumentNullException("uriPrefix"); + + bool result = prefixes.Remove(uriPrefix); + if (result && listener.IsListening) + EndPointManager.RemovePrefix(_logger, uriPrefix, listener); + + return result; + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs new file mode 100644 index 000000000..63d5e510d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs @@ -0,0 +1,654 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerRequest + { + string[] accept_types; + Encoding content_encoding; + long content_length; + bool cl_set; + CookieCollection cookies; + WebHeaderCollection headers; + string method; + Stream input_stream; + Version version; + QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness + string raw_url; + Uri url; + Uri referrer; + string[] user_languages; + HttpListenerContext context; + bool is_chunked; + bool ka_set; + bool keep_alive; + + private readonly ITextEncoding _textEncoding; + + internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding) + { + this.context = context; + _textEncoding = textEncoding; + headers = new WebHeaderCollection(); + version = HttpVersion.Version10; + } + + static char[] separators = new char[] { ' ' }; + + internal void SetRequestLine(string req) + { + string[] parts = req.Split(separators, 3); + if (parts.Length != 3) + { + context.ErrorMessage = "Invalid request line (parts)."; + return; + } + + method = parts[0]; + foreach (char c in method) + { + int ic = (int)c; + + if ((ic >= 'A' && ic <= 'Z') || + (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && + c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && + c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && + c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) + continue; + + context.ErrorMessage = "(Invalid verb)"; + return; + } + + raw_url = parts[1]; + if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + + try + { + version = new Version(parts[2].Substring(5)); + if (version.Major < 1) + throw new Exception(); + } + catch + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + } + + void CreateQueryString(string query) + { + if (query == null || query.Length == 0) + { + query_string = new QueryParamCollection(); + return; + } + + query_string = new QueryParamCollection(); + if (query[0] == '?') + query = query.Substring(1); + string[] components = query.Split('&'); + foreach (string kv in components) + { + int pos = kv.IndexOf('='); + if (pos == -1) + { + query_string.Add(null, WebUtility.UrlDecode(kv)); + } + else + { + string key = WebUtility.UrlDecode(kv.Substring(0, pos)); + string val = WebUtility.UrlDecode(kv.Substring(pos + 1)); + + query_string.Add(key, val); + } + } + } + + internal void FinishInitialization() + { + string host = UserHostName; + if (version > HttpVersion.Version10 && (host == null || host.Length == 0)) + { + context.ErrorMessage = "Invalid host name"; + return; + } + + string path; + Uri raw_uri = null; + if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri)) + path = raw_uri.PathAndQuery; + else + path = raw_url; + + if ((host == null || host.Length == 0)) + host = UserHostAddress; + + if (raw_uri != null) + host = raw_uri.Host; + + int colon = host.LastIndexOf(':'); + if (colon >= 0) + host = host.Substring(0, colon); + + string base_uri = String.Format("{0}://{1}:{2}", + (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"), + host, LocalEndPoint.Port); + + if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url)) + { + context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path); + return; return; + } + + CreateQueryString(url.Query); + + if (version >= HttpVersion.Version11) + { + string t_encoding = Headers["Transfer-Encoding"]; + is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0); + // 'identity' is not valid! + if (t_encoding != null && !is_chunked) + { + context.Connection.SendError(null, 501); + return; + } + } + + if (!is_chunked && !cl_set) + { + if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0) + { + context.Connection.SendError(null, 411); + return; + } + } + + if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) + { + ResponseStream output = context.Connection.GetResponseStream(); + + var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); + + output.InternalWrite(_100continue, 0, _100continue.Length); + } + } + + static bool MaybeUri(string s) + { + int p = s.IndexOf(':'); + if (p == -1) + return false; + + if (p >= 10) + return false; + + return IsPredefinedScheme(s.Substring(0, p)); + } + + // + // Using a simple block of if's is twice as slow as the compiler generated + // switch statement. But using this tuned code is faster than the + // compiler generated code, with a million loops on x86-64: + // + // With "http": .10 vs .51 (first check) + // with "https": .16 vs .51 (second check) + // with "foo": .22 vs .31 (never found) + // with "mailto": .12 vs .51 (last check) + // + // + static bool IsPredefinedScheme(string scheme) + { + if (scheme == null || scheme.Length < 3) + return false; + + char c = scheme[0]; + if (c == 'h') + return (scheme == "http" || scheme == "https"); + if (c == 'f') + return (scheme == "file" || scheme == "ftp"); + + if (c == 'n') + { + c = scheme[1]; + if (c == 'e') + return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp"); + if (scheme == "nntp") + return true; + return false; + } + if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto")) + return true; + + return false; + } + + internal static string Unquote(String str) + { + int start = str.IndexOf('\"'); + int end = str.LastIndexOf('\"'); + if (start >= 0 && end >= 0) + str = str.Substring(start + 1, end - 1); + return str.Trim(); + } + + internal void AddHeader(string header) + { + int colon = header.IndexOf(':'); + if (colon == -1 || colon == 0) + { + context.ErrorMessage = "Bad Request"; + context.ErrorStatus = 400; + return; + } + + string name = header.Substring(0, colon).Trim(); + string val = header.Substring(colon + 1).Trim(); + string lower = name.ToLowerInvariant(); + headers.SetInternal(name, val); + switch (lower) + { + case "accept-language": + user_languages = val.Split(','); // yes, only split with a ',' + break; + case "accept": + accept_types = val.Split(','); // yes, only split with a ',' + break; + case "content-length": + try + { + //TODO: max. content_length? + content_length = Int64.Parse(val.Trim()); + if (content_length < 0) + context.ErrorMessage = "Invalid Content-Length."; + cl_set = true; + } + catch + { + context.ErrorMessage = "Invalid Content-Length."; + } + + break; + case "content-type": + { + var contents = val.Split(';'); + foreach (var content in contents) + { + var tmp = content.Trim(); + if (tmp.StartsWith("charset")) + { + var charset = tmp.GetValue("="); + if (charset != null && charset.Length > 0) + { + try + { + + // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n + charset = charset.Trim('"'); + var index = charset.IndexOf('"'); + if (index != -1) charset = charset.Substring(0, index); + + content_encoding = Encoding.GetEncoding(charset); + } + catch + { + context.ErrorMessage = "Invalid Content-Type header: " + charset; + } + } + + break; + } + } + } + break; + case "referer": + try + { + referrer = new Uri(val); + } + catch + { + referrer = new Uri("http://someone.is.screwing.with.the.headers.com/"); + } + break; + case "cookie": + if (cookies == null) + cookies = new CookieCollection(); + + string[] cookieStrings = val.Split(new char[] { ',', ';' }); + Cookie current = null; + int version = 0; + foreach (string cookieString in cookieStrings) + { + string str = cookieString.Trim(); + if (str.Length == 0) + continue; + if (str.StartsWith("$Version")) + { + version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1))); + } + else if (str.StartsWith("$Path")) + { + if (current != null) + current.Path = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Domain")) + { + if (current != null) + current.Domain = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Port")) + { + if (current != null) + current.Port = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else + { + if (current != null) + { + cookies.Add(current); + } + current = new Cookie(); + int idx = str.IndexOf('='); + if (idx > 0) + { + current.Name = str.Substring(0, idx).Trim(); + current.Value = str.Substring(idx + 1).Trim(); + } + else + { + current.Name = str.Trim(); + current.Value = String.Empty; + } + current.Version = version; + } + } + if (current != null) + { + cookies.Add(current); + } + break; + } + } + + // returns true is the stream could be reused. + internal bool FlushInput() + { + if (!HasEntityBody) + return true; + + int length = 2048; + if (content_length > 0) + length = (int)Math.Min(content_length, (long)length); + + byte[] bytes = new byte[length]; + while (true) + { + // TODO: test if MS has a timeout when doing this + try + { + var task = InputStream.ReadAsync(bytes, 0, length); + var result = Task.WaitAll(new [] { task }, 1000); + if (!result) + { + return false; + } + if (task.Result <= 0) + { + return true; + } + } + catch (ObjectDisposedException e) + { + input_stream = null; + return true; + } + catch + { + return false; + } + } + } + + public string[] AcceptTypes + { + get { return accept_types; } + } + + public int ClientCertificateError + { + get + { + HttpConnection cnc = context.Connection; + //if (cnc.ClientCertificate == null) + // throw new InvalidOperationException("No client certificate"); + //int[] errors = cnc.ClientCertificateErrors; + //if (errors != null && errors.Length > 0) + // return errors[0]; + return 0; + } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + } + + public long ContentLength64 + { + get { return content_length; } + } + + public string ContentType + { + get { return headers["content-type"]; } + } + + public CookieCollection Cookies + { + get + { + // TODO: check if the collection is read-only + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + } + + public bool HasEntityBody + { + get { return (content_length > 0 || is_chunked); } + } + + public QueryParamCollection Headers + { + get { return headers; } + } + + public string HttpMethod + { + get { return method; } + } + + public Stream InputStream + { + get + { + if (input_stream == null) + { + if (is_chunked || content_length > 0) + input_stream = context.Connection.GetRequestStream(is_chunked, content_length); + else + input_stream = Stream.Null; + } + + return input_stream; + } + } + + public bool IsAuthenticated + { + get { return false; } + } + + public bool IsLocal + { + get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); } + } + + public bool IsSecureConnection + { + get { return context.Connection.IsSecure; } + } + + public bool KeepAlive + { + get + { + if (ka_set) + return keep_alive; + + ka_set = true; + // 1. Connection header + // 2. Protocol (1.1 == keep-alive by default) + // 3. Keep-Alive header + string cnc = headers["Connection"]; + if (!String.IsNullOrEmpty(cnc)) + { + keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase)); + } + else if (version == HttpVersion.Version11) + { + keep_alive = true; + } + else + { + cnc = headers["keep-alive"]; + if (!String.IsNullOrEmpty(cnc)) + keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase)); + } + return keep_alive; + } + } + + public IpEndPointInfo LocalEndPoint + { + get { return context.Connection.LocalEndPoint; } + } + + public Version ProtocolVersion + { + get { return version; } + } + + public QueryParamCollection QueryString + { + get { return query_string; } + } + + public string RawUrl + { + get { return raw_url; } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return context.Connection.RemoteEndPoint; } + } + + public Guid RequestTraceIdentifier + { + get { return Guid.Empty; } + } + + public Uri Url + { + get { return url; } + } + + public Uri UrlReferrer + { + get { return referrer; } + } + + public string UserAgent + { + get { return headers["user-agent"]; } + } + + public string UserHostAddress + { + get { return LocalEndPoint.ToString(); } + } + + public string UserHostName + { + get { return headers["host"]; } + } + + public string[] UserLanguages + { + get { return user_languages; } + } + + public string ServiceName + { + get + { + return null; + } + } + + private bool _websocketRequestWasSet; + private bool _websocketRequest; + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public bool IsWebSocketRequest + { + get + { + if (!_websocketRequestWasSet) + { + _websocketRequest = method == "GET" && + version > HttpVersion.Version10 && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + + _websocketRequestWasSet = true; + } + + return _websocketRequest; + } + } + + public Task GetClientCertificateAsync() + { + return Task.FromResult(null); + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs new file mode 100644 index 000000000..0bc827b5a --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs @@ -0,0 +1,517 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerResponse : IDisposable + { + bool disposed; + Encoding content_encoding; + long content_length; + bool cl_set; + string content_type; + CookieCollection cookies; + WebHeaderCollection headers = new WebHeaderCollection(); + bool keep_alive = true; + ResponseStream output_stream; + Version version = HttpVersion.Version11; + string location; + int status_code = 200; + string status_description = "OK"; + bool chunked; + HttpListenerContext context; + + internal bool HeadersSent; + internal object headers_lock = new object(); + + bool force_close_chunked; + + private readonly ILogger _logger; + private readonly ITextEncoding _textEncoding; + + internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding) + { + this.context = context; + _logger = logger; + _textEncoding = textEncoding; + } + + internal bool CloseConnection + { + get + { + return headers["Connection"] == "close"; + } + } + + internal bool ForceCloseChunked + { + get { return force_close_chunked; } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + content_encoding = value; + } + } + + public long ContentLength64 + { + get { return content_length; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (HeadersSent) + throw new InvalidOperationException("Cannot be changed after headers are sent."); + + if (value < 0) + throw new ArgumentOutOfRangeException("Must be >= 0", "value"); + + cl_set = true; + content_length = value; + } + } + + public string ContentType + { + get { return content_type; } + set + { + // TODO: is null ok? + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + content_type = value; + } + } + + // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html + public CookieCollection Cookies + { + get + { + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + set { cookies = value; } // null allowed? + } + + public WebHeaderCollection Headers + { + get { return headers; } + set + { + /** + * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or + * WWW-Authenticate header using the Headers property, an exception will be + * thrown. Use the KeepAlive or ContentLength64 properties to set these headers. + * You cannot set the Transfer-Encoding or WWW-Authenticate headers manually." + */ + // TODO: check if this is marked readonly after headers are sent. + headers = value; + } + } + + public bool KeepAlive + { + get { return keep_alive; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + keep_alive = value; + } + } + + public Stream OutputStream + { + get + { + if (output_stream == null) + output_stream = context.Connection.GetResponseStream(); + return output_stream; + } + } + + public Version ProtocolVersion + { + get { return version; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (value == null) + throw new ArgumentNullException("value"); + + if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) + throw new ArgumentException("Must be 1.0 or 1.1", "value"); + + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + version = value; + } + } + + public string RedirectLocation + { + get { return location; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + location = value; + } + } + + public bool SendChunked + { + get { return chunked; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + chunked = value; + } + } + + public int StatusCode + { + get { return status_code; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (value < 100 || value > 999) + throw new ProtocolViolationException("StatusCode must be between 100 and 999."); + status_code = value; + status_description = GetStatusDescription(value); + } + } + + internal static string GetStatusDescription(int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + return ""; + } + + public string StatusDescription + { + get { return status_description; } + set + { + status_description = value; + } + } + + void IDisposable.Dispose() + { + Close(true); //TODO: Abort or Close? + } + + public void Abort() + { + if (disposed) + return; + + Close(true); + } + + public void AddHeader(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + if (name == "") + throw new ArgumentException("'name' cannot be empty", "name"); + + //TODO: check for forbidden headers and invalid characters + if (value.Length > 65535) + throw new ArgumentOutOfRangeException("value"); + + headers.Set(name, value); + } + + public void AppendCookie(Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException("cookie"); + + Cookies.Add(cookie); + } + + public void AppendHeader(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + if (name == "") + throw new ArgumentException("'name' cannot be empty", "name"); + + if (value.Length > 65535) + throw new ArgumentOutOfRangeException("value"); + + headers.Add(name, value); + } + + void Close(bool force) + { + if (force) + { + _logger.Debug("HttpListenerResponse force closing HttpConnection"); + } + disposed = true; + context.Connection.Close(force); + } + + public void Close() + { + if (disposed) + return; + + Close(false); + } + + public void Close(byte[] responseEntity, bool willBlock) + { + if (disposed) + return; + + if (responseEntity == null) + throw new ArgumentNullException("responseEntity"); + + //TODO: if willBlock -> BeginWrite + Close ? + ContentLength64 = responseEntity.Length; + OutputStream.Write(responseEntity, 0, (int)content_length); + Close(false); + } + + public void Redirect(string url) + { + StatusCode = 302; // Found + location = url; + } + + bool FindCookie(Cookie cookie) + { + string name = cookie.Name; + string domain = cookie.Domain; + string path = cookie.Path; + foreach (Cookie c in cookies) + { + if (name != c.Name) + continue; + if (domain != c.Domain) + continue; + if (path == c.Path) + return true; + } + + return false; + } + + internal void SendHeaders(bool closing, MemoryStream ms) + { + Encoding encoding = content_encoding; + if (encoding == null) + encoding = _textEncoding.GetDefaultEncoding(); + + if (content_type != null) + { + if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.Ordinal) == -1) + { + string enc_name = content_encoding.WebName; + headers.SetInternal("Content-Type", content_type + "; charset=" + enc_name); + } + else + { + headers.SetInternal("Content-Type", content_type); + } + } + + if (headers["Server"] == null) + headers.SetInternal("Server", "Mono-HTTPAPI/1.0"); + + CultureInfo inv = CultureInfo.InvariantCulture; + if (headers["Date"] == null) + headers.SetInternal("Date", DateTime.UtcNow.ToString("r", inv)); + + if (!chunked) + { + if (!cl_set && closing) + { + cl_set = true; + content_length = 0; + } + + if (cl_set) + headers.SetInternal("Content-Length", content_length.ToString(inv)); + } + + Version v = context.Request.ProtocolVersion; + if (!cl_set && !chunked && v >= HttpVersion.Version11) + chunked = true; + + /* Apache forces closing the connection for these status codes: + * HttpStatusCode.BadRequest 400 + * HttpStatusCode.RequestTimeout 408 + * HttpStatusCode.LengthRequired 411 + * HttpStatusCode.RequestEntityTooLarge 413 + * HttpStatusCode.RequestUriTooLong 414 + * HttpStatusCode.InternalServerError 500 + * HttpStatusCode.ServiceUnavailable 503 + */ + bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || + status_code == 413 || status_code == 414 || status_code == 500 || + status_code == 503); + + if (conn_close == false) + conn_close = !context.Request.KeepAlive; + + // They sent both KeepAlive: true and Connection: close!? + if (!keep_alive || conn_close) + { + headers.SetInternal("Connection", "close"); + conn_close = true; + } + + if (chunked) + headers.SetInternal("Transfer-Encoding", "chunked"); + + //int reuses = context.Connection.Reuses; + //if (reuses >= 100) + //{ + // _logger.Debug("HttpListenerResponse - keep alive has exceeded 100 uses and will be closed."); + + // force_close_chunked = true; + // if (!conn_close) + // { + // headers.SetInternal("Connection", "close"); + // conn_close = true; + // } + //} + + if (!conn_close) + { + if (context.Request.ProtocolVersion <= HttpVersion.Version10) + headers.SetInternal("Connection", "keep-alive"); + } + + if (location != null) + headers.SetInternal("Location", location); + + if (cookies != null) + { + foreach (Cookie cookie in cookies) + headers.SetInternal("Set-Cookie", cookie.ToString()); + } + + using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true)) + { + writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description); + string headers_str = headers.ToStringMultiValue(); + writer.Write(headers_str); + writer.Flush(); + } + + int preamble = encoding.GetPreamble().Length; + if (output_stream == null) + output_stream = context.Connection.GetResponseStream(); + + /* Assumes that the ms was at position 0 */ + ms.Position = preamble; + HeadersSent = true; + } + + public void SetCookie(Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException("cookie"); + + if (cookies != null) + { + if (FindCookie(cookie)) + throw new ArgumentException("The cookie already exists."); + } + else + { + cookies = new CookieCollection(); + } + + cookies.Add(cookie); + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/HttpStatusCode.cs b/SocketHttpListener.Portable/Net/HttpStatusCode.cs new file mode 100644 index 000000000..93da82ba0 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpStatusCode.cs @@ -0,0 +1,321 @@ +namespace SocketHttpListener.Net +{ + /// + /// Contains the values of the HTTP status codes. + /// + /// + /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in + /// RFC 2616 for HTTP 1.1. + /// + public enum HttpStatusCode + { + /// + /// Equivalent to status code 100. + /// Indicates that the client should continue with its request. + /// + Continue = 100, + /// + /// Equivalent to status code 101. + /// Indicates that the server is switching the HTTP version or protocol on the connection. + /// + SwitchingProtocols = 101, + /// + /// Equivalent to status code 200. + /// Indicates that the client's request has succeeded. + /// + OK = 200, + /// + /// Equivalent to status code 201. + /// Indicates that the client's request has been fulfilled and resulted in a new resource being + /// created. + /// + Created = 201, + /// + /// Equivalent to status code 202. + /// Indicates that the client's request has been accepted for processing, but the processing + /// hasn't been completed. + /// + Accepted = 202, + /// + /// Equivalent to status code 203. + /// Indicates that the returned metainformation is from a local or a third-party copy instead of + /// the origin server. + /// + NonAuthoritativeInformation = 203, + /// + /// Equivalent to status code 204. + /// Indicates that the server has fulfilled the client's request but doesn't need to return + /// an entity-body. + /// + NoContent = 204, + /// + /// Equivalent to status code 205. + /// Indicates that the server has fulfilled the client's request, and the user agent should + /// reset the document view which caused the request to be sent. + /// + ResetContent = 205, + /// + /// Equivalent to status code 206. + /// Indicates that the server has fulfilled the partial GET request for the resource. + /// + PartialContent = 206, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// MultipleChoices is a synonym for Ambiguous. + /// + /// + MultipleChoices = 300, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// Ambiguous is a synonym for MultipleChoices. + /// + /// + Ambiguous = 300, + /// + /// + /// Equivalent to status code 301. + /// Indicates that the requested resource has been assigned a new permanent URI and + /// any future references to this resource should use one of the returned URIs. + /// + /// + /// MovedPermanently is a synonym for Moved. + /// + /// + MovedPermanently = 301, + /// + /// + /// Equivalent to status code 301. + /// Indicates that the requested resource has been assigned a new permanent URI and + /// any future references to this resource should use one of the returned URIs. + /// + /// + /// Moved is a synonym for MovedPermanently. + /// + /// + Moved = 301, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Found is a synonym for Redirect. + /// + /// + Found = 302, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Redirect is a synonym for Found. + /// + /// + Redirect = 302, + /// + /// + /// Equivalent to status code 303. + /// Indicates that the response to the request can be found under a different URI and + /// should be retrieved using a GET method on that resource. + /// + /// + /// SeeOther is a synonym for RedirectMethod. + /// + /// + SeeOther = 303, + /// + /// + /// Equivalent to status code 303. + /// Indicates that the response to the request can be found under a different URI and + /// should be retrieved using a GET method on that resource. + /// + /// + /// RedirectMethod is a synonym for SeeOther. + /// + /// + RedirectMethod = 303, + /// + /// Equivalent to status code 304. + /// Indicates that the client has performed a conditional GET request and access is allowed, + /// but the document hasn't been modified. + /// + NotModified = 304, + /// + /// Equivalent to status code 305. + /// Indicates that the requested resource must be accessed through the proxy given by + /// the Location field. + /// + UseProxy = 305, + /// + /// Equivalent to status code 306. + /// This status code was used in a previous version of the specification, is no longer used, + /// and is reserved for future use. + /// + Unused = 306, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// TemporaryRedirect is a synonym for RedirectKeepVerb. + /// + /// + TemporaryRedirect = 307, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// RedirectKeepVerb is a synonym for TemporaryRedirect. + /// + /// + RedirectKeepVerb = 307, + /// + /// Equivalent to status code 400. + /// Indicates that the client's request couldn't be understood by the server due to + /// malformed syntax. + /// + BadRequest = 400, + /// + /// Equivalent to status code 401. + /// Indicates that the client's request requires user authentication. + /// + Unauthorized = 401, + /// + /// Equivalent to status code 402. + /// This status code is reserved for future use. + /// + PaymentRequired = 402, + /// + /// Equivalent to status code 403. + /// Indicates that the server understood the client's request but is refusing to fulfill it. + /// + Forbidden = 403, + /// + /// Equivalent to status code 404. + /// Indicates that the server hasn't found anything matching the request URI. + /// + NotFound = 404, + /// + /// Equivalent to status code 405. + /// Indicates that the method specified in the request line isn't allowed for the resource + /// identified by the request URI. + /// + MethodNotAllowed = 405, + /// + /// Equivalent to status code 406. + /// Indicates that the server doesn't have the appropriate resource to respond to the Accept + /// headers in the client's request. + /// + NotAcceptable = 406, + /// + /// Equivalent to status code 407. + /// Indicates that the client must first authenticate itself with the proxy. + /// + ProxyAuthenticationRequired = 407, + /// + /// Equivalent to status code 408. + /// Indicates that the client didn't produce a request within the time that the server was + /// prepared to wait. + /// + RequestTimeout = 408, + /// + /// Equivalent to status code 409. + /// Indicates that the client's request couldn't be completed due to a conflict on the server. + /// + Conflict = 409, + /// + /// Equivalent to status code 410. + /// Indicates that the requested resource is no longer available at the server and + /// no forwarding address is known. + /// + Gone = 410, + /// + /// Equivalent to status code 411. + /// Indicates that the server refuses to accept the client's request without a defined + /// Content-Length. + /// + LengthRequired = 411, + /// + /// Equivalent to status code 412. + /// Indicates that the precondition given in one or more of the request headers evaluated to + /// false when it was tested on the server. + /// + PreconditionFailed = 412, + /// + /// Equivalent to status code 413. + /// Indicates that the entity of the client's request is larger than the server is willing or + /// able to process. + /// + RequestEntityTooLarge = 413, + /// + /// Equivalent to status code 414. + /// Indicates that the request URI is longer than the server is willing to interpret. + /// + RequestUriTooLong = 414, + /// + /// Equivalent to status code 415. + /// Indicates that the entity of the client's request is in a format not supported by + /// the requested resource for the requested method. + /// + UnsupportedMediaType = 415, + /// + /// Equivalent to status code 416. + /// Indicates that none of the range specifier values in a Range request header overlap + /// the current extent of the selected resource. + /// + RequestedRangeNotSatisfiable = 416, + /// + /// Equivalent to status code 417. + /// Indicates that the expectation given in an Expect request header couldn't be met by + /// the server. + /// + ExpectationFailed = 417, + /// + /// Equivalent to status code 500. + /// Indicates that the server encountered an unexpected condition which prevented it from + /// fulfilling the client's request. + /// + InternalServerError = 500, + /// + /// Equivalent to status code 501. + /// Indicates that the server doesn't support the functionality required to fulfill the client's + /// request. + /// + NotImplemented = 501, + /// + /// Equivalent to status code 502. + /// Indicates that a gateway or proxy server received an invalid response from the upstream + /// server. + /// + BadGateway = 502, + /// + /// Equivalent to status code 503. + /// Indicates that the server is currently unable to handle the client's request due to + /// a temporary overloading or maintenance of the server. + /// + ServiceUnavailable = 503, + /// + /// Equivalent to status code 504. + /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream + /// server or some other auxiliary server. + /// + GatewayTimeout = 504, + /// + /// Equivalent to status code 505. + /// Indicates that the server doesn't support the HTTP version used in the client's request. + /// + HttpVersionNotSupported = 505, + } +} diff --git a/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs b/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs new file mode 100644 index 000000000..518c45acb --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; + +namespace SocketHttpListener.Net +{ + class HttpStreamAsyncResult : IAsyncResult + { + object locker = new object(); + ManualResetEvent handle; + bool completed; + + internal byte[] Buffer; + internal int Offset; + internal int Count; + internal AsyncCallback Callback; + internal object State; + internal int SynchRead; + internal Exception Error; + + public void Complete(Exception e) + { + Error = e; + Complete(); + } + + public void Complete() + { + lock (locker) + { + if (completed) + return; + + completed = true; + if (handle != null) + handle.Set(); + + if (Callback != null) + Callback.BeginInvoke(this, null, null); + } + } + + public object AsyncState + { + get { return State; } + } + + public WaitHandle AsyncWaitHandle + { + get + { + lock (locker) + { + if (handle == null) + handle = new ManualResetEvent(completed); + } + + return handle; + } + } + + public bool CompletedSynchronously + { + get { return (SynchRead == Count); } + } + + public bool IsCompleted + { + get + { + lock (locker) + { + return completed; + } + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpVersion.cs b/SocketHttpListener.Portable/Net/HttpVersion.cs new file mode 100644 index 000000000..c0839b46d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpVersion.cs @@ -0,0 +1,16 @@ +using System; + +namespace SocketHttpListener.Net +{ + // + // + public class HttpVersion + { + + public static readonly Version Version10 = new Version(1, 0); + public static readonly Version Version11 = new Version(1, 1); + + // pretty useless.. + public HttpVersion() { } + } +} diff --git a/SocketHttpListener.Portable/Net/ListenerPrefix.cs b/SocketHttpListener.Portable/Net/ListenerPrefix.cs new file mode 100644 index 000000000..2c314da50 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ListenerPrefix.cs @@ -0,0 +1,148 @@ +using System; +using System.Net; +using MediaBrowser.Model.Net; + +namespace SocketHttpListener.Net +{ + sealed class ListenerPrefix + { + string original; + string host; + ushort port; + string path; + bool secure; + IpAddressInfo[] addresses; + public HttpListener Listener; + + public ListenerPrefix(string prefix) + { + this.original = prefix; + Parse(prefix); + } + + public override string ToString() + { + return original; + } + + public IpAddressInfo[] Addresses + { + get { return addresses; } + set { addresses = value; } + } + public bool Secure + { + get { return secure; } + } + + public string Host + { + get { return host; } + } + + public int Port + { + get { return (int)port; } + } + + public string Path + { + get { return path; } + } + + // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection. + public override bool Equals(object o) + { + ListenerPrefix other = o as ListenerPrefix; + if (other == null) + return false; + + return (original == other.original); + } + + public override int GetHashCode() + { + return original.GetHashCode(); + } + + void Parse(string uri) + { + ushort default_port = 80; + if (uri.StartsWith("https://")) + { + default_port = 443; + secure = true; + } + + int length = uri.Length; + int start_host = uri.IndexOf(':') + 3; + if (start_host >= length) + throw new ArgumentException("No host specified."); + + int colon = uri.IndexOf(':', start_host, length - start_host); + int root; + if (colon > 0) + { + host = uri.Substring(start_host, colon - start_host); + root = uri.IndexOf('/', colon, length - colon); + port = (ushort)Int32.Parse(uri.Substring(colon + 1, root - colon - 1)); + path = uri.Substring(root); + } + else + { + root = uri.IndexOf('/', start_host, length - start_host); + host = uri.Substring(start_host, root - start_host); + port = default_port; + path = uri.Substring(root); + } + if (path.Length != 1) + path = path.Substring(0, path.Length - 1); + } + + public static void CheckUri(string uri) + { + if (uri == null) + throw new ArgumentNullException("uriPrefix"); + + if (!uri.StartsWith("http://") && !uri.StartsWith("https://")) + throw new ArgumentException("Only 'http' and 'https' schemes are supported."); + + int length = uri.Length; + int start_host = uri.IndexOf(':') + 3; + if (start_host >= length) + throw new ArgumentException("No host specified."); + + int colon = uri.IndexOf(':', start_host, length - start_host); + if (start_host == colon) + throw new ArgumentException("No host specified."); + + int root; + if (colon > 0) + { + root = uri.IndexOf('/', colon, length - colon); + if (root == -1) + throw new ArgumentException("No path specified."); + + try + { + int p = Int32.Parse(uri.Substring(colon + 1, root - colon - 1)); + if (p <= 0 || p >= 65536) + throw new Exception(); + } + catch + { + throw new ArgumentException("Invalid port."); + } + } + else + { + root = uri.IndexOf('/', start_host, length - start_host); + if (root == -1) + throw new ArgumentException("No path specified."); + } + + if (uri[uri.Length - 1] != '/') + throw new ArgumentException("The prefix must end with '/'"); + } + } +} diff --git a/SocketHttpListener.Portable/Net/RequestStream.cs b/SocketHttpListener.Portable/Net/RequestStream.cs new file mode 100644 index 000000000..58030500d --- /dev/null +++ b/SocketHttpListener.Portable/Net/RequestStream.cs @@ -0,0 +1,231 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + class RequestStream : Stream + { + byte[] buffer; + int offset; + int length; + long remaining_body; + bool disposed; + Stream stream; + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length) + : this(stream, buffer, offset, length, -1) + { + } + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length, long contentlength) + { + this.stream = stream; + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.remaining_body = contentlength; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + + protected override void Dispose(bool disposing) + { + disposed = true; + } + + public override void Flush() + { + } + + + // Returns 0 if we can keep reading from the base stream, + // > 0 if we read something from the buffer. + // -1 if we had a content length set and we finished reading that many bytes. + int FillFromBuffer(byte[] buffer, int off, int count) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (off < 0) + throw new ArgumentOutOfRangeException("offset", "< 0"); + if (count < 0) + throw new ArgumentOutOfRangeException("count", "< 0"); + int len = buffer.Length; + if (off > len) + throw new ArgumentException("destination offset is beyond array size"); + if (off > len - count) + throw new ArgumentException("Reading would overrun buffer"); + + if (this.remaining_body == 0) + return -1; + + if (this.length == 0) + return 0; + + int size = Math.Min(this.length, count); + if (this.remaining_body > 0) + size = (int)Math.Min(size, this.remaining_body); + + if (this.offset > this.buffer.Length - size) + { + size = Math.Min(size, this.buffer.Length - this.offset); + } + if (size == 0) + return 0; + + Buffer.BlockCopy(this.buffer, this.offset, buffer, off, size); + this.offset += size; + this.length -= size; + if (this.remaining_body > 0) + remaining_body -= size; + return size; + } + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 + int nread = FillFromBuffer(buffer, offset, count); + if (nread == -1) + { // No more bytes available (Content-Length) + return 0; + } + else if (nread > 0) + { + return nread; + } + + nread = stream.Read(buffer, offset, count); + if (nread > 0 && remaining_body > 0) + remaining_body -= nread; + return nread; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (disposed) + throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + int nread = FillFromBuffer(buffer, offset, count); + if (nread > 0 || nread == -1) + { + return Math.Max(0, nread); + } + + // Avoid reading past the end of the request to allow + // for HTTP pipelining + if (remaining_body >= 0 && count > remaining_body) + count = (int)Math.Min(Int32.MaxValue, remaining_body); + + nread = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + if (remaining_body > 0 && nread > 0) + remaining_body -= nread; + return nread; + } + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // int nread = FillFromBuffer(buffer, offset, count); + // if (nread > 0 || nread == -1) + // { + // HttpStreamAsyncResult ares = new HttpStreamAsyncResult(); + // ares.Buffer = buffer; + // ares.Offset = offset; + // ares.Count = count; + // ares.Callback = cback; + // ares.State = state; + // ares.SynchRead = Math.Max(0, nread); + // ares.Complete(); + // return ares; + // } + + // // Avoid reading past the end of the request to allow + // // for HTTP pipelining + // if (remaining_body >= 0 && count > remaining_body) + // count = (int)Math.Min(Int32.MaxValue, remaining_body); + // return stream.BeginRead(buffer, offset, count, cback, state); + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // if (ares == null) + // throw new ArgumentNullException("async_result"); + + // if (ares is HttpStreamAsyncResult) + // { + // HttpStreamAsyncResult r = (HttpStreamAsyncResult)ares; + // if (!ares.IsCompleted) + // ares.AsyncWaitHandle.WaitOne(); + // return r.SynchRead; + // } + + // // Close on exception? + // int nread = stream.EndRead(ares); + // if (remaining_body > 0 && nread > 0) + // remaining_body -= nread; + // return nread; + //} + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // throw new NotSupportedException(); + //} + + //public override void EndWrite(IAsyncResult async_result) + //{ + // throw new NotSupportedException(); + //} + } +} diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs new file mode 100644 index 000000000..6ecbf9742 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -0,0 +1,316 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + // FIXME: Does this buffer the response until Close? + // Update: we send a single packet for the first non-chunked Write + // What happens when we set content-length to X and write X-1 bytes then close? + // what if we don't set content-length at all? + class ResponseStream : Stream + { + HttpListenerResponse response; + bool ignore_errors; + bool disposed; + bool trailer_sent; + Stream stream; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + + internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.response = response; + this.ignore_errors = ignore_errors; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + this.stream = stream; + } + + public override bool CanRead + { + get { return false; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + + protected override void Dispose(bool disposing) + { + if (disposed == false) + { + disposed = true; + byte[] bytes = null; + MemoryStream ms = GetHeaders(true); + bool chunked = response.SendChunked; + if (stream.CanWrite) + { + try + { + if (ms != null) + { + long start = ms.Position; + if (chunked && !trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + ms.Position = ms.Length; + ms.Write(bytes, 0, bytes.Length); + } + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + InternalWrite(msBuffer, (int)start, (int)(ms.Length - start)); + trailer_sent = true; + } + else if (chunked && !trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + InternalWrite(bytes, 0, bytes.Length); + trailer_sent = true; + } + } + catch (IOException ex) + { + // Ignore error due to connection reset by peer + } + } + response.Close(); + } + + base.Dispose(disposing); + } + + MemoryStream GetHeaders(bool closing) + { + // SendHeaders works on shared headers + lock (response.headers_lock) + { + if (response.HeadersSent) + return null; + MemoryStream ms = _memoryStreamFactory.CreateNew(); + response.SendHeaders(closing, ms); + return ms; + } + } + + public override void Flush() + { + } + + static byte[] crlf = new byte[] { 13, 10 }; + byte[] GetChunkSizeBytes(int size, bool final) + { + string str = String.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); + return _textEncoding.GetASCIIEncoding().GetBytes(str); + } + + internal void InternalWrite(byte[] buffer, int offset, int count) + { + if (ignore_errors) + { + try + { + stream.Write(buffer, offset, count); + } + catch { } + } + else + { + stream.Write(buffer, offset, count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = response.SendChunked; + if (ms != null) + { + long start = ms.Position; // After the possible preamble for the encoding + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + + int new_count = Math.Min(count, 16384 - (int)ms.Position + (int)start); + ms.Write(buffer, offset, new_count); + count -= new_count; + offset += new_count; + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + InternalWrite(msBuffer, (int)start, (int)(ms.Length - start)); + ms.SetLength(0); + ms.Capacity = 0; // 'dispose' the buffer in ms. + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + if (count > 0) + InternalWrite(buffer, offset, count); + if (chunked) + InternalWrite(crlf, 0, 2); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = response.SendChunked; + if (ms != null) + { + long start = ms.Position; + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + ms.Write(buffer, offset, count); + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + buffer = msBuffer; + offset = (int)start; + count = (int)(ms.Position - start); + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + try + { + if (count > 0) + { + await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + if (response.SendChunked) + stream.Write(crlf, 0, 2); + } + catch + { + if (!ignore_errors) + { + throw; + } + } + } + + //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // byte[] bytes = null; + // MemoryStream ms = GetHeaders(false); + // bool chunked = response.SendChunked; + // if (ms != null) + // { + // long start = ms.Position; + // ms.Position = ms.Length; + // if (chunked) + // { + // bytes = GetChunkSizeBytes(count, false); + // ms.Write(bytes, 0, bytes.Length); + // } + // ms.Write(buffer, offset, count); + // buffer = ms.ToArray(); + // offset = (int)start; + // count = (int)(ms.Position - start); + // } + // else if (chunked) + // { + // bytes = GetChunkSizeBytes(count, false); + // InternalWrite(bytes, 0, bytes.Length); + // } + + // return stream.BeginWrite(buffer, offset, count, cback, state); + //} + + //public override void EndWrite(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // if (ignore_errors) + // { + // try + // { + // stream.EndWrite(ares); + // if (response.SendChunked) + // stream.Write(crlf, 0, 2); + // } + // catch { } + // } + // else { + // stream.EndWrite(ares); + // if (response.SendChunked) + // stream.Write(crlf, 0, 2); + // } + //} + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // throw new NotSupportedException(); + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // throw new NotSupportedException(); + //} + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + } +} diff --git a/SocketHttpListener.Portable/Net/WebHeaderCollection.cs b/SocketHttpListener.Portable/Net/WebHeaderCollection.cs new file mode 100644 index 000000000..d20f99b9b --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebHeaderCollection.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener.Net +{ + [ComVisible(true)] + public class WebHeaderCollection : QueryParamCollection + { + [Flags] + internal enum HeaderInfo + { + Request = 1, + Response = 1 << 1, + MultiValue = 1 << 10 + } + + static readonly bool[] allowed_chars = { + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, true, false, true, true, true, true, false, false, false, true, + true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false, + false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, true, false + }; + + static readonly Dictionary headers; + HeaderInfo? headerRestriction; + HeaderInfo? headerConsistency; + + static WebHeaderCollection() + { + headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { + { "Allow", HeaderInfo.MultiValue }, + { "Accept", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Accept-Charset", HeaderInfo.MultiValue }, + { "Accept-Encoding", HeaderInfo.MultiValue }, + { "Accept-Language", HeaderInfo.MultiValue }, + { "Accept-Ranges", HeaderInfo.MultiValue }, + { "Age", HeaderInfo.Response }, + { "Authorization", HeaderInfo.MultiValue }, + { "Cache-Control", HeaderInfo.MultiValue }, + { "Cookie", HeaderInfo.MultiValue }, + { "Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Content-Encoding", HeaderInfo.MultiValue }, + { "Content-Length", HeaderInfo.Request | HeaderInfo.Response }, + { "Content-Type", HeaderInfo.Request }, + { "Content-Language", HeaderInfo.MultiValue }, + { "Date", HeaderInfo.Request }, + { "Expect", HeaderInfo.Request | HeaderInfo.MultiValue}, + { "Host", HeaderInfo.Request }, + { "If-Match", HeaderInfo.MultiValue }, + { "If-Modified-Since", HeaderInfo.Request }, + { "If-None-Match", HeaderInfo.MultiValue }, + { "Keep-Alive", HeaderInfo.Response }, + { "Pragma", HeaderInfo.MultiValue }, + { "Proxy-Authenticate", HeaderInfo.MultiValue }, + { "Proxy-Authorization", HeaderInfo.MultiValue }, + { "Proxy-Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Range", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Referer", HeaderInfo.Request }, + { "Set-Cookie", HeaderInfo.MultiValue }, + { "Set-Cookie2", HeaderInfo.MultiValue }, + { "Server", HeaderInfo.Response }, + { "TE", HeaderInfo.MultiValue }, + { "Trailer", HeaderInfo.MultiValue }, + { "Transfer-Encoding", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo.MultiValue }, + { "Translate", HeaderInfo.Request | HeaderInfo.Response }, + { "Upgrade", HeaderInfo.MultiValue }, + { "User-Agent", HeaderInfo.Request }, + { "Vary", HeaderInfo.MultiValue }, + { "Via", HeaderInfo.MultiValue }, + { "Warning", HeaderInfo.MultiValue }, + { "WWW-Authenticate", HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketAccept", HeaderInfo.Response }, + { "SecWebSocketExtensions", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketKey", HeaderInfo.Request }, + { "Sec-WebSocket-Protocol", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketVersion", HeaderInfo.Response | HeaderInfo. MultiValue } + }; + } + + // Methods + + public void Add(string header) + { + if (header == null) + throw new ArgumentNullException("header"); + int pos = header.IndexOf(':'); + if (pos == -1) + throw new ArgumentException("no colon found", "header"); + + this.Add(header.Substring(0, pos), header.Substring(pos + 1)); + } + + public override void Add(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + ThrowIfRestricted(name); + this.AddWithoutValidate(name, value); + } + + protected void AddWithoutValidate(string headerName, string headerValue) + { + if (!IsHeaderName(headerName)) + throw new ArgumentException("invalid header name: " + headerName, "headerName"); + if (headerValue == null) + headerValue = String.Empty; + else + headerValue = headerValue.Trim(); + if (!IsHeaderValue(headerValue)) + throw new ArgumentException("invalid header value: " + headerValue, "headerValue"); + + AddValue(headerName, headerValue); + } + + internal void AddValue(string headerName, string headerValue) + { + base.Add(headerName, headerValue); + } + + internal string[] GetValues_internal(string header, bool split) + { + if (header == null) + throw new ArgumentNullException("header"); + + string[] values = base.GetValues(header); + if (values == null || values.Length == 0) + return null; + + if (split && IsMultiValue(header)) + { + List separated = null; + foreach (var value in values) + { + if (value.IndexOf(',') < 0) + { + if (separated != null) + separated.Add(value); + + continue; + } + + if (separated == null) + { + separated = new List(values.Length + 1); + foreach (var v in values) + { + if (v == value) + break; + + separated.Add(v); + } + } + + var slices = value.Split(','); + var slices_length = slices.Length; + if (value[value.Length - 1] == ',') + --slices_length; + + for (int i = 0; i < slices_length; ++i) + { + separated.Add(slices[i].Trim()); + } + } + + if (separated != null) + return separated.ToArray(); + } + + return values; + } + + public override string[] GetValues(string header) + { + return GetValues_internal(header, true); + } + + public override string[] GetValues(int index) + { + string[] values = base.GetValues(index); + + if (values == null || values.Length == 0) + { + return null; + } + + return values; + } + + public static bool IsRestricted(string headerName) + { + return IsRestricted(headerName, false); + } + + public static bool IsRestricted(string headerName, bool response) + { + if (headerName == null) + throw new ArgumentNullException("headerName"); + + if (headerName.Length == 0) + throw new ArgumentException("empty string", "headerName"); + + if (!IsHeaderName(headerName)) + throw new ArgumentException("Invalid character in header"); + + HeaderInfo info; + if (!headers.TryGetValue(headerName, out info)) + return false; + + var flag = response ? HeaderInfo.Response : HeaderInfo.Request; + return (info & flag) != 0; + } + + public override void Set(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + if (!IsHeaderName(name)) + throw new ArgumentException("invalid header name"); + if (value == null) + value = String.Empty; + else + value = value.Trim(); + if (!IsHeaderValue(value)) + throw new ArgumentException("invalid header value"); + + ThrowIfRestricted(name); + base.Set(name, value); + } + + internal string ToStringMultiValue() + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count; i++) + { + string key = GetKey(i); + if (IsMultiValue(key)) + { + foreach (string v in GetValues(i)) + { + sb.Append(key) + .Append(": ") + .Append(v) + .Append("\r\n"); + } + } + else + { + sb.Append(key) + .Append(": ") + .Append(Get(i)) + .Append("\r\n"); + } + } + return sb.Append("\r\n").ToString(); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count; i++) + sb.Append(GetKey(i)) + .Append(": ") + .Append(Get(i)) + .Append("\r\n"); + + return sb.Append("\r\n").ToString(); + } + + + // Internal Methods + + // With this we don't check for invalid characters in header. See bug #55994. + internal void SetInternal(string header) + { + int pos = header.IndexOf(':'); + if (pos == -1) + throw new ArgumentException("no colon found", "header"); + + SetInternal(header.Substring(0, pos), header.Substring(pos + 1)); + } + + internal void SetInternal(string name, string value) + { + if (value == null) + value = String.Empty; + else + value = value.Trim(); + if (!IsHeaderValue(value)) + throw new ArgumentException("invalid header value"); + + if (IsMultiValue(name)) + { + base.Add(name, value); + } + else + { + base.Remove(name); + base.Set(name, value); + } + } + + // Private Methods + + public override int Remove(string name) + { + ThrowIfRestricted(name); + return base.Remove(name); + } + + protected void ThrowIfRestricted(string headerName) + { + if (!headerRestriction.HasValue) + return; + + HeaderInfo info; + if (!headers.TryGetValue(headerName, out info)) + return; + + if ((info & headerRestriction.Value) != 0) + throw new ArgumentException("This header must be modified with the appropriate property."); + } + + internal static bool IsMultiValue(string headerName) + { + if (headerName == null) + return false; + + HeaderInfo info; + return headers.TryGetValue(headerName, out info) && (info & HeaderInfo.MultiValue) != 0; + } + + internal static bool IsHeaderValue(string value) + { + // TEXT any 8 bit value except CTL's (0-31 and 127) + // but including \r\n space and \t + // after a newline at least one space or \t must follow + // certain header fields allow comments () + + int len = value.Length; + for (int i = 0; i < len; i++) + { + char c = value[i]; + if (c == 127) + return false; + if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t')) + return false; + if (c == '\n' && ++i < len) + { + c = value[i]; + if (c != ' ' && c != '\t') + return false; + } + } + + return true; + } + + internal static bool IsHeaderName(string name) + { + if (name == null || name.Length == 0) + return false; + + int len = name.Length; + for (int i = 0; i < len; i++) + { + char c = name[i]; + if (c > 126 || !allowed_chars[c]) + return false; + } + + return true; + } + } +} diff --git a/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs b/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs new file mode 100644 index 000000000..426e15ecd --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net.WebSockets +{ + /// + /// Provides the properties used to access the information in a WebSocket connection request + /// received by the . + /// + /// + /// + public class HttpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private HttpListenerContext _context; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal HttpListenerWebSocketContext( + HttpListenerContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory) + { + _context = context; + _websocket = new WebSocket(this, protocol, cryptoProvider, memoryStreamFactory); + } + + #endregion + + #region Internal Properties + + internal Stream Stream + { + get + { + return _context.Connection.Stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// A that contains the cookies. + /// + public override CookieCollection CookieCollection + { + get + { + return _context.Request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public override QueryParamCollection Headers + { + get + { + return _context.Request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the request. + /// + /// + /// A that represents the value of the Host header. + /// + public override string Host + { + get + { + return _context.Request.Headers["Host"]; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated + { + get + { + return _context.Request.IsAuthenticated; + } + } + + /// + /// Gets a value indicating whether the client connected from the local computer. + /// + /// + /// true if the client connected from the local computer; otherwise, false. + /// + public override bool IsLocal + { + get + { + return _context.Request.IsLocal; + } + } + + /// + /// Gets a value indicating whether the WebSocket connection is secured. + /// + /// + /// true if the connection is secured; otherwise, false. + /// + public override bool IsSecureConnection + { + get + { + return _context.Connection.IsSecure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public override bool IsWebSocketRequest + { + get + { + return _context.Request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the request. + /// + /// + /// A that represents the value of the Origin header. + /// + public override string Origin + { + get + { + return _context.Request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the request. + /// + /// + /// A that contains the query string parameters. + /// + public override QueryParamCollection QueryString + { + get + { + return _context.Request.QueryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the requested URI. + /// + public override Uri RequestUri + { + get + { + return _context.Request.Url; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// + /// + /// This property provides a part of the information used by the server to prove that it + /// received a valid WebSocket connection request. + /// + /// + /// A that represents the value of the Sec-WebSocket-Key header. + /// + public override string SecWebSocketKey + { + get + { + return _context.Request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// + /// + /// This property represents the subprotocols requested by the client. + /// + /// + /// An instance that provides + /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol + /// header. + /// + public override IEnumerable SecWebSocketProtocols + { + get + { + var protocols = _context.Request.Headers["Sec-WebSocket-Protocol"]; + if (protocols != null) + foreach (var protocol in protocols.Split(',')) + yield return protocol.Trim(); + } + } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// + /// + /// This property represents the WebSocket protocol version. + /// + /// + /// A that represents the value of the Sec-WebSocket-Version header. + /// + public override string SecWebSocketVersion + { + get + { + return _context.Request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// + public override IpEndPointInfo ServerEndPoint + { + get + { + return _context.Connection.LocalEndPoint; + } + } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A that represents the client information. + /// + public override IPrincipal User + { + get + { + return _context.User; + } + } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// + public override IpEndPointInfo UserEndPoint + { + get + { + return _context.Connection.RemoteEndPoint; + } + } + + /// + /// Gets the instance used for two-way communication + /// between client and server. + /// + /// + /// A . + /// + public override WebSocket WebSocket + { + get + { + return _websocket; + } + } + + #endregion + + #region Internal Methods + + internal void Close() + { + try + { + _context.Connection.Close(true); + } + catch + { + // catch errors sending the closing handshake + } + } + + internal void Close(HttpStatusCode code) + { + _context.Response.Close(code); + } + + #endregion + + #region Public Methods + + /// + /// Returns a that represents the current + /// . + /// + /// + /// A that represents the current + /// . + /// + public override string ToString() + { + return _context.Request.ToString(); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs b/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs new file mode 100644 index 000000000..3ffa6e639 --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener.Net.WebSockets +{ + /// + /// Exposes the properties used to access the information in a WebSocket connection request. + /// + /// + /// The WebSocketContext class is an abstract class. + /// + public abstract class WebSocketContext + { + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketContext() + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// A that contains the cookies. + /// + public abstract CookieCollection CookieCollection { get; } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public abstract QueryParamCollection Headers { get; } + + /// + /// Gets the value of the Host header included in the request. + /// + /// + /// A that represents the value of the Host header. + /// + public abstract string Host { get; } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public abstract bool IsAuthenticated { get; } + + /// + /// Gets a value indicating whether the client connected from the local computer. + /// + /// + /// true if the client connected from the local computer; otherwise, false. + /// + public abstract bool IsLocal { get; } + + /// + /// Gets a value indicating whether the WebSocket connection is secured. + /// + /// + /// true if the connection is secured; otherwise, false. + /// + public abstract bool IsSecureConnection { get; } + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public abstract bool IsWebSocketRequest { get; } + + /// + /// Gets the value of the Origin header included in the request. + /// + /// + /// A that represents the value of the Origin header. + /// + public abstract string Origin { get; } + + /// + /// Gets the query string included in the request. + /// + /// + /// A that contains the query string parameters. + /// + public abstract QueryParamCollection QueryString { get; } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the requested URI. + /// + public abstract Uri RequestUri { get; } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// + /// + /// This property provides a part of the information used by the server to prove that it + /// received a valid WebSocket connection request. + /// + /// + /// A that represents the value of the Sec-WebSocket-Key header. + /// + public abstract string SecWebSocketKey { get; } + + /// + /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// + /// + /// This property represents the subprotocols requested by the client. + /// + /// + /// An instance that provides + /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol + /// header. + /// + public abstract IEnumerable SecWebSocketProtocols { get; } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// + /// + /// This property represents the WebSocket protocol version. + /// + /// + /// A that represents the value of the Sec-WebSocket-Version header. + /// + public abstract string SecWebSocketVersion { get; } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// A that represents the server endpoint. + /// + public abstract IpEndPointInfo ServerEndPoint { get; } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A that represents the client information. + /// + public abstract IPrincipal User { get; } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// A that represents the client endpoint. + /// + public abstract IpEndPointInfo UserEndPoint { get; } + + /// + /// Gets the instance used for two-way communication + /// between client and server. + /// + /// + /// A . + /// + public abstract WebSocket WebSocket { get; } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Opcode.cs b/SocketHttpListener.Portable/Opcode.cs new file mode 100644 index 000000000..62b7d8585 --- /dev/null +++ b/SocketHttpListener.Portable/Opcode.cs @@ -0,0 +1,43 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the opcode that indicates the type of a WebSocket frame. + /// + /// + /// The values of the opcode are defined in + /// Section 5.2 of RFC 6455. + /// + public enum Opcode : byte + { + /// + /// Equivalent to numeric value 0. + /// Indicates a continuation frame. + /// + Cont = 0x0, + /// + /// Equivalent to numeric value 1. + /// Indicates a text frame. + /// + Text = 0x1, + /// + /// Equivalent to numeric value 2. + /// Indicates a binary frame. + /// + Binary = 0x2, + /// + /// Equivalent to numeric value 8. + /// Indicates a connection close frame. + /// + Close = 0x8, + /// + /// Equivalent to numeric value 9. + /// Indicates a ping frame. + /// + Ping = 0x9, + /// + /// Equivalent to numeric value 10. + /// Indicates a pong frame. + /// + Pong = 0xa + } +} diff --git a/SocketHttpListener.Portable/PayloadData.cs b/SocketHttpListener.Portable/PayloadData.cs new file mode 100644 index 000000000..a6318da2b --- /dev/null +++ b/SocketHttpListener.Portable/PayloadData.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace SocketHttpListener +{ + internal class PayloadData : IEnumerable + { + #region Private Fields + + private byte [] _applicationData; + private byte [] _extensionData; + private bool _masked; + + #endregion + + #region Public Const Fields + + public const ulong MaxLength = long.MaxValue; + + #endregion + + #region Public Constructors + + public PayloadData () + : this (new byte [0], new byte [0], false) + { + } + + public PayloadData (byte [] applicationData) + : this (new byte [0], applicationData, false) + { + } + + public PayloadData (string applicationData) + : this (new byte [0], Encoding.UTF8.GetBytes (applicationData), false) + { + } + + public PayloadData (byte [] applicationData, bool masked) + : this (new byte [0], applicationData, masked) + { + } + + public PayloadData (byte [] extensionData, byte [] applicationData, bool masked) + { + _extensionData = extensionData; + _applicationData = applicationData; + _masked = masked; + } + + #endregion + + #region Internal Properties + + internal bool ContainsReservedCloseStatusCode { + get { + return _applicationData.Length > 1 && + _applicationData.SubArray (0, 2).ToUInt16 (ByteOrder.Big).IsReserved (); + } + } + + #endregion + + #region Public Properties + + public byte [] ApplicationData { + get { + return _applicationData; + } + } + + public byte [] ExtensionData { + get { + return _extensionData; + } + } + + public bool IsMasked { + get { + return _masked; + } + } + + public ulong Length { + get { + return (ulong) (_extensionData.Length + _applicationData.Length); + } + } + + #endregion + + #region Private Methods + + private static void mask (byte [] src, byte [] key) + { + for (long i = 0; i < src.Length; i++) + src [i] = (byte) (src [i] ^ key [i % 4]); + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (byte b in _extensionData) + yield return b; + + foreach (byte b in _applicationData) + yield return b; + } + + public void Mask (byte [] maskingKey) + { + if (_extensionData.Length > 0) + mask (_extensionData, maskingKey); + + if (_applicationData.Length > 0) + mask (_applicationData, maskingKey); + + _masked = !_masked; + } + + public byte [] ToByteArray () + { + return _extensionData.Length > 0 + ? new List (this).ToArray () + : _applicationData; + } + + public override string ToString () + { + return BitConverter.ToString (ToByteArray ()); + } + + #endregion + + #region Explicitly Implemented Interface Members + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Primitives/HttpListenerException.cs b/SocketHttpListener.Portable/Primitives/HttpListenerException.cs new file mode 100644 index 000000000..7b383fd23 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/HttpListenerException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Primitives +{ + public class HttpListenerException : Exception + { + public HttpListenerException(int statusCode, string message) + : base(message) + { + + } + } +} diff --git a/SocketHttpListener.Portable/Primitives/ICertificate.cs b/SocketHttpListener.Portable/Primitives/ICertificate.cs new file mode 100644 index 000000000..1289da13d --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/ICertificate.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Primitives +{ + public interface ICertificate + { + } +} diff --git a/SocketHttpListener.Portable/Primitives/IStreamFactory.cs b/SocketHttpListener.Portable/Primitives/IStreamFactory.cs new file mode 100644 index 000000000..f189b95b4 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/IStreamFactory.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; + +namespace SocketHttpListener.Primitives +{ + public interface IStreamFactory + { + Stream CreateNetworkStream(ISocket socket, bool ownsSocket); + Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen); + + Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate); + } +} diff --git a/SocketHttpListener.Portable/Primitives/ITextEncoding.cs b/SocketHttpListener.Portable/Primitives/ITextEncoding.cs new file mode 100644 index 000000000..b10145687 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/ITextEncoding.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Text; + +namespace SocketHttpListener.Primitives +{ + public static class TextEncodingExtensions + { + public static Encoding GetDefaultEncoding(this ITextEncoding encoding) + { + return Encoding.UTF8; + } + } +} diff --git a/SocketHttpListener.Portable/Properties/AssemblyInfo.cs b/SocketHttpListener.Portable/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..870426460 --- /dev/null +++ b/SocketHttpListener.Portable/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SocketHttpListener.Portable")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SocketHttpListener.Portable")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SocketHttpListener.Portable/Rsv.cs b/SocketHttpListener.Portable/Rsv.cs new file mode 100644 index 000000000..668059b8a --- /dev/null +++ b/SocketHttpListener.Portable/Rsv.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Rsv : byte + { + Off = 0x0, + On = 0x1 + } +} diff --git a/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj b/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj new file mode 100644 index 000000000..f7b3a643c --- /dev/null +++ b/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj @@ -0,0 +1,109 @@ + + + + + 11.0 + Debug + AnyCPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E} + Library + Properties + SocketHttpListener.Portable + SocketHttpListener.Portable + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + if $(ConfigurationName) == Release ( +xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i +) + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets b/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocket.cs b/SocketHttpListener.Portable/WebSocket.cs new file mode 100644 index 000000000..889880387 --- /dev/null +++ b/SocketHttpListener.Portable/WebSocket.cs @@ -0,0 +1,898 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using SocketHttpListener.Net.WebSockets; +using SocketHttpListener.Primitives; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; + +namespace SocketHttpListener +{ + /// + /// Implements the WebSocket interface. + /// + /// + /// The WebSocket class provides a set of methods and properties for two-way communication using + /// the WebSocket protocol (RFC 6455). + /// + public class WebSocket : IDisposable + { + #region Private Fields + + private string _base64Key; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private CookieCollection _cookies; + private string _extensions; + private AutoResetEvent _exitReceiving; + private object _forConn; + private object _forEvent; + private object _forMessageEventQueue; + private object _forSend; + private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private Func + _handshakeRequestChecker; + private Queue _messageEventQueue; + private uint _nonceCount; + private string _origin; + private bool _preAuth; + private string _protocol; + private string[] _protocols; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private AutoResetEvent _receivePong; + private bool _secure; + private Stream _stream; + private Uri _uri; + private const string _version = "13"; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + private readonly ICryptoProvider _cryptoProvider; + + #endregion + + #region Internal Fields + + internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14. + + #endregion + + #region Internal Constructors + + // As server + internal WebSocket(HttpListenerWebSocketContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory) + { + _context = context; + _protocol = protocol; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + + _closeContext = context.Close; + _secure = context.IsSecureConnection; + _stream = context.Stream; + + init(); + } + + #endregion + + // As server + internal Func CustomHandshakeRequestChecker + { + get + { + return _handshakeRequestChecker ?? (context => null); + } + + set + { + _handshakeRequestChecker = value; + } + } + + internal bool IsConnected + { + get + { + return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; + } + } + + /// + /// Gets the state of the WebSocket connection. + /// + /// + /// One of the enum values, indicates the state of the WebSocket + /// connection. The default value is . + /// + public WebSocketState ReadyState + { + get + { + return _readyState; + } + } + + #region Public Events + + /// + /// Occurs when the WebSocket connection has been closed. + /// + public event EventHandler OnClose; + + /// + /// Occurs when the gets an error. + /// + public event EventHandler OnError; + + /// + /// Occurs when the receives a message. + /// + public event EventHandler OnMessage; + + /// + /// Occurs when the WebSocket connection has been established. + /// + public event EventHandler OnOpen; + + #endregion + + #region Private Methods + + // As server + private bool acceptHandshake() + { + var msg = checkIfValidHandshakeRequest(_context); + if (msg != null) + { + error("An error has occurred while connecting: " + msg); + Close(HttpStatusCode.BadRequest); + + return false; + } + + if (_protocol != null && + !_context.SecWebSocketProtocols.Contains(protocol => protocol == _protocol)) + _protocol = null; + + ////var extensions = _context.Headers["Sec-WebSocket-Extensions"]; + ////if (extensions != null && extensions.Length > 0) + //// processSecWebSocketExtensionsHeader(extensions); + + return sendHttpResponse(createHandshakeResponse()); + } + + // As server + private string checkIfValidHandshakeRequest(WebSocketContext context) + { + var headers = context.Headers; + return context.RequestUri == null + ? "Invalid request url." + : !context.IsWebSocketRequest + ? "Not WebSocket connection request." + : !validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"]) + ? "Invalid Sec-WebSocket-Key header." + : !validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"]) + ? "Invalid Sec-WebSocket-Version header." + : CustomHandshakeRequestChecker(context); + } + + private void close(CloseStatusCode code, string reason, bool wait) + { + close(new PayloadData(((ushort)code).Append(reason)), !code.IsReserved(), wait); + } + + private void close(PayloadData payload, bool send, bool wait) + { + lock (_forConn) + { + if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) + { + return; + } + + _readyState = WebSocketState.Closing; + } + + var e = new CloseEventArgs(payload); + e.WasClean = + closeHandshake( + send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null, + wait ? 1000 : 0, + closeServerResources); + + _readyState = WebSocketState.Closed; + try + { + OnClose.Emit(this, e); + } + catch (Exception ex) + { + error("An exception has occurred while OnClose.", ex); + } + } + + private bool closeHandshake(byte[] frameAsBytes, int millisecondsTimeout, Action release) + { + var sent = frameAsBytes != null && writeBytes(frameAsBytes); + var received = + millisecondsTimeout == 0 || + (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout)); + + release(); + if (_receivePong != null) + { + _receivePong.Dispose(); + _receivePong = null; + } + + if (_exitReceiving != null) + { + _exitReceiving.Dispose(); + _exitReceiving = null; + } + + var result = sent && received; + + return result; + } + + // As server + private void closeServerResources() + { + if (_closeContext == null) + return; + + _closeContext(); + _closeContext = null; + _stream = null; + _context = null; + } + + private bool concatenateFragmentsInto(Stream dest) + { + while (true) + { + var frame = WebSocketFrame.Read(_stream, true); + if (frame.IsFinal) + { + /* FINAL */ + + // CONT + if (frame.IsContinuation) + { + dest.WriteBytes(frame.PayloadData.ApplicationData); + break; + } + + // PING + if (frame.IsPing) + { + processPingFrame(frame); + continue; + } + + // PONG + if (frame.IsPong) + { + processPongFrame(frame); + continue; + } + + // CLOSE + if (frame.IsClose) + return processCloseFrame(frame); + } + else + { + /* MORE */ + + // CONT + if (frame.IsContinuation) + { + dest.WriteBytes(frame.PayloadData.ApplicationData); + continue; + } + } + + // ? + return processUnsupportedFrame( + frame, + CloseStatusCode.IncorrectData, + "An incorrect data has been received while receiving fragmented data."); + } + + return true; + } + + // As server + private HttpResponse createHandshakeCloseResponse(HttpStatusCode code) + { + var res = HttpResponse.CreateCloseResponse(code); + res.Headers["Sec-WebSocket-Version"] = _version; + + return res; + } + + // As server + private HttpResponse createHandshakeResponse() + { + var res = HttpResponse.CreateWebSocketResponse(); + + var headers = res.Headers; + headers["Sec-WebSocket-Accept"] = CreateResponseKey(_base64Key); + + if (_protocol != null) + headers["Sec-WebSocket-Protocol"] = _protocol; + + if (_extensions != null) + headers["Sec-WebSocket-Extensions"] = _extensions; + + if (_cookies.Count > 0) + res.SetCookies(_cookies); + + return res; + } + + private MessageEventArgs dequeueFromMessageEventQueue() + { + lock (_forMessageEventQueue) + return _messageEventQueue.Count > 0 + ? _messageEventQueue.Dequeue() + : null; + } + + private void enqueueToMessageEventQueue(MessageEventArgs e) + { + lock (_forMessageEventQueue) + _messageEventQueue.Enqueue(e); + } + + private void error(string message, Exception exception) + { + try + { + if (exception != null) + { + message += ". Exception.Message: " + exception.Message; + } + OnError.Emit(this, new ErrorEventArgs(message)); + } + catch (Exception ex) + { + } + } + + private void error(string message) + { + try + { + OnError.Emit(this, new ErrorEventArgs(message)); + } + catch (Exception ex) + { + } + } + + private void init() + { + _compression = CompressionMethod.None; + _cookies = new CookieCollection(); + _forConn = new object(); + _forEvent = new object(); + _forSend = new object(); + _messageEventQueue = new Queue(); + _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; + _readyState = WebSocketState.Connecting; + } + + private void open() + { + try + { + startReceiving(); + + lock (_forEvent) + { + try + { + OnOpen.Emit(this, EventArgs.Empty); + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while OnOpen."); + } + } + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while opening."); + } + } + + private bool processCloseFrame(WebSocketFrame frame) + { + var payload = frame.PayloadData; + close(payload, !payload.ContainsReservedCloseStatusCode, false); + + return false; + } + + private bool processDataFrame(WebSocketFrame frame) + { + var e = frame.IsCompressed + ? new MessageEventArgs( + frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) + : new MessageEventArgs(frame.Opcode, frame.PayloadData); + + enqueueToMessageEventQueue(e); + return true; + } + + private void processException(Exception exception, string message) + { + var code = CloseStatusCode.Abnormal; + var reason = message; + if (exception is WebSocketException) + { + var wsex = (WebSocketException)exception; + code = wsex.Code; + reason = wsex.Message; + } + + error(message ?? code.GetMessage(), exception); + if (_readyState == WebSocketState.Connecting) + Close(HttpStatusCode.BadRequest); + else + close(code, reason ?? code.GetMessage(), false); + } + + private bool processFragmentedFrame(WebSocketFrame frame) + { + return frame.IsContinuation // Not first fragment + ? true + : processFragments(frame); + } + + private bool processFragments(WebSocketFrame first) + { + using (var buff = _memoryStreamFactory.CreateNew()) + { + buff.WriteBytes(first.PayloadData.ApplicationData); + if (!concatenateFragmentsInto(buff)) + return false; + + byte[] data; + if (_compression != CompressionMethod.None) + { + data = buff.DecompressToArray(_compression); + } + else + { + data = buff.ToArray(); + } + + enqueueToMessageEventQueue(new MessageEventArgs(first.Opcode, data)); + return true; + } + } + + private bool processPingFrame(WebSocketFrame frame) + { + var mask = Mask.Unmask; + + return true; + } + + private bool processPongFrame(WebSocketFrame frame) + { + _receivePong.Set(); + + return true; + } + + private bool processUnsupportedFrame(WebSocketFrame frame, CloseStatusCode code, string reason) + { + processException(new WebSocketException(code, reason), null); + + return false; + } + + private bool processWebSocketFrame(WebSocketFrame frame) + { + return frame.IsCompressed && _compression == CompressionMethod.None + ? processUnsupportedFrame( + frame, + CloseStatusCode.IncorrectData, + "A compressed data has been received without available decompression method.") + : frame.IsFragmented + ? processFragmentedFrame(frame) + : frame.IsData + ? processDataFrame(frame) + : frame.IsPing + ? processPingFrame(frame) + : frame.IsPong + ? processPongFrame(frame) + : frame.IsClose + ? processCloseFrame(frame) + : processUnsupportedFrame(frame, CloseStatusCode.PolicyViolation, null); + } + + private bool send(Opcode opcode, Stream stream) + { + lock (_forSend) + { + var src = stream; + var compressed = false; + var sent = false; + try + { + if (_compression != CompressionMethod.None) + { + stream = stream.Compress(_compression); + compressed = true; + } + + sent = send(opcode, Mask.Unmask, stream, compressed); + if (!sent) + error("Sending a data has been interrupted."); + } + catch (Exception ex) + { + error("An exception has occurred while sending a data.", ex); + } + finally + { + if (compressed) + stream.Dispose(); + + src.Dispose(); + } + + return sent; + } + } + + private bool send(Opcode opcode, Mask mask, Stream stream, bool compressed) + { + var len = stream.Length; + + /* Not fragmented */ + + if (len == 0) + return send(Fin.Final, opcode, mask, new byte[0], compressed); + + var quo = len / FragmentLength; + var rem = (int)(len % FragmentLength); + + byte[] buff = null; + if (quo == 0) + { + buff = new byte[rem]; + return stream.Read(buff, 0, rem) == rem && + send(Fin.Final, opcode, mask, buff, compressed); + } + + buff = new byte[FragmentLength]; + if (quo == 1 && rem == 0) + return stream.Read(buff, 0, FragmentLength) == FragmentLength && + send(Fin.Final, opcode, mask, buff, compressed); + + /* Send fragmented */ + + // Begin + if (stream.Read(buff, 0, FragmentLength) != FragmentLength || + !send(Fin.More, opcode, mask, buff, compressed)) + return false; + + var n = rem == 0 ? quo - 2 : quo - 1; + for (long i = 0; i < n; i++) + if (stream.Read(buff, 0, FragmentLength) != FragmentLength || + !send(Fin.More, Opcode.Cont, mask, buff, compressed)) + return false; + + // End + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + return stream.Read(buff, 0, rem) == rem && + send(Fin.Final, Opcode.Cont, mask, buff, compressed); + } + + private bool send(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + { + lock (_forConn) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + return writeBytes( + WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray()); + } + } + + private void sendAsync(Opcode opcode, Stream stream, Action completed) + { + Func sender = send; + sender.BeginInvoke( + opcode, + stream, + ar => + { + try + { + var sent = sender.EndInvoke(ar); + if (completed != null) + completed(sent); + } + catch (Exception ex) + { + error("An exception has occurred while callback.", ex); + } + }, + null); + } + + // As server + private bool sendHttpResponse(HttpResponse response) + { + return writeBytes(response.ToByteArray()); + } + + private void startReceiving() + { + if (_messageEventQueue.Count > 0) + _messageEventQueue.Clear(); + + _exitReceiving = new AutoResetEvent(false); + _receivePong = new AutoResetEvent(false); + + Action receive = null; + receive = () => WebSocketFrame.ReadAsync( + _stream, + true, + frame => + { + if (processWebSocketFrame(frame) && _readyState != WebSocketState.Closed) + { + receive(); + + if (!frame.IsData) + return; + + lock (_forEvent) + { + try + { + var e = dequeueFromMessageEventQueue(); + if (e != null && _readyState == WebSocketState.Open) + OnMessage.Emit(this, e); + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while OnMessage."); + } + } + } + else if (_exitReceiving != null) + { + _exitReceiving.Set(); + } + }, + ex => processException(ex, "An exception has occurred while receiving a message.")); + + receive(); + } + + // As server + private bool validateSecWebSocketKeyHeader(string value) + { + if (value == null || value.Length == 0) + return false; + + _base64Key = value; + return true; + } + + // As server + private bool validateSecWebSocketVersionClientHeader(string value) + { + return true; + //return value != null && value == _version; + } + + private bool writeBytes(byte[] data) + { + try + { + _stream.Write(data, 0, data.Length); + return true; + } + catch (Exception ex) + { + return false; + } + } + + #endregion + + #region Internal Methods + + // As server + internal void Close(HttpResponse response) + { + _readyState = WebSocketState.Closing; + + sendHttpResponse(response); + closeServerResources(); + + _readyState = WebSocketState.Closed; + } + + // As server + internal void Close(HttpStatusCode code) + { + Close(createHandshakeCloseResponse(code)); + } + + // As server + public void ConnectAsServer() + { + try + { + if (acceptHandshake()) + { + _readyState = WebSocketState.Open; + open(); + } + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while connecting."); + } + } + + private string CreateResponseKey(string base64Key) + { + var buff = new StringBuilder(base64Key, 64); + buff.Append(_guid); + var src = _cryptoProvider.ComputeSHA1(Encoding.UTF8.GetBytes(buff.ToString())); + + return Convert.ToBase64String(src); + } + + #endregion + + #region Public Methods + + /// + /// Closes the WebSocket connection, and releases all associated resources. + /// + public void Close() + { + var msg = _readyState.CheckIfClosable(); + if (msg != null) + { + error(msg); + + return; + } + + var send = _readyState == WebSocketState.Open; + close(new PayloadData(), send, send); + } + + /// + /// Closes the WebSocket connection with the specified + /// and , and releases all associated resources. + /// + /// + /// This method emits a event if the size + /// of is greater than 123 bytes. + /// + /// + /// One of the enum values, represents the status code + /// indicating the reason for the close. + /// + /// + /// A that represents the reason for the close. + /// + public void Close(CloseStatusCode code, string reason) + { + byte[] data = null; + var msg = _readyState.CheckIfClosable() ?? + (data = ((ushort)code).Append(reason)).CheckIfValidControlData("reason"); + + if (msg != null) + { + error(msg); + + return; + } + + var send = _readyState == WebSocketState.Open && !code.IsReserved(); + close(new PayloadData(data), send, send); + } + + /// + /// Sends a binary asynchronously using the WebSocket connection. + /// + /// + /// This method doesn't wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// An Action<bool> delegate that references the method(s) called when the send is + /// complete. A passed to this delegate is true if the send is + /// complete successfully; otherwise, false. + /// + public void SendAsync(byte[] data, Action completed) + { + var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); + if (msg != null) + { + error(msg); + + return; + } + + sendAsync(Opcode.Binary, _memoryStreamFactory.CreateNew(data), completed); + } + + /// + /// Sends a text asynchronously using the WebSocket connection. + /// + /// + /// This method doesn't wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// An Action<bool> delegate that references the method(s) called when the send is + /// complete. A passed to this delegate is true if the send is + /// complete successfully; otherwise, false. + /// + public void SendAsync(string data, Action completed) + { + var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); + if (msg != null) + { + error(msg); + + return; + } + + sendAsync(Opcode.Text, _memoryStreamFactory.CreateNew(Encoding.UTF8.GetBytes(data)), completed); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// Closes the WebSocket connection, and releases all associated resources. + /// + /// + /// This method closes the WebSocket connection with . + /// + void IDisposable.Dispose() + { + Close(CloseStatusCode.Away, null); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocketException.cs b/SocketHttpListener.Portable/WebSocketException.cs new file mode 100644 index 000000000..260721317 --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketException.cs @@ -0,0 +1,60 @@ +using System; + +namespace SocketHttpListener +{ + /// + /// The exception that is thrown when a gets a fatal error. + /// + public class WebSocketException : Exception + { + #region Internal Constructors + + internal WebSocketException () + : this (CloseStatusCode.Abnormal, null, null) + { + } + + internal WebSocketException (string message) + : this (CloseStatusCode.Abnormal, message, null) + { + } + + internal WebSocketException (CloseStatusCode code) + : this (code, null, null) + { + } + + internal WebSocketException (string message, Exception innerException) + : this (CloseStatusCode.Abnormal, message, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, string message) + : this (code, message, null) + { + } + + internal WebSocketException (CloseStatusCode code, string message, Exception innerException) + : base (message ?? code.GetMessage (), innerException) + { + Code = code; + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code indicating the cause for the exception. + /// + /// + /// One of the enum values, represents the status code indicating + /// the cause for the exception. + /// + public CloseStatusCode Code { + get; private set; + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/WebSocketFrame.cs b/SocketHttpListener.Portable/WebSocketFrame.cs new file mode 100644 index 000000000..44fa4a5dc --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketFrame.cs @@ -0,0 +1,578 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SocketHttpListener +{ + internal class WebSocketFrame : IEnumerable + { + #region Private Fields + + private byte[] _extPayloadLength; + private Fin _fin; + private Mask _mask; + private byte[] _maskingKey; + private Opcode _opcode; + private PayloadData _payloadData; + private byte _payloadLength; + private Rsv _rsv1; + private Rsv _rsv2; + private Rsv _rsv3; + + #endregion + + #region Internal Fields + + internal static readonly byte[] EmptyUnmaskPingData; + + #endregion + + #region Static Constructor + + static WebSocketFrame() + { + EmptyUnmaskPingData = CreatePingFrame(Mask.Unmask).ToByteArray(); + } + + #endregion + + #region Private Constructors + + private WebSocketFrame() + { + } + + #endregion + + #region Internal Constructors + + internal WebSocketFrame(Opcode opcode, PayloadData payload) + : this(Fin.Final, opcode, Mask.Mask, payload, false) + { + } + + internal WebSocketFrame(Opcode opcode, Mask mask, PayloadData payload) + : this(Fin.Final, opcode, mask, payload, false) + { + } + + internal WebSocketFrame(Fin fin, Opcode opcode, Mask mask, PayloadData payload) + : this(fin, opcode, mask, payload, false) + { + } + + internal WebSocketFrame( + Fin fin, Opcode opcode, Mask mask, PayloadData payload, bool compressed) + { + _fin = fin; + _rsv1 = isData(opcode) && compressed ? Rsv.On : Rsv.Off; + _rsv2 = Rsv.Off; + _rsv3 = Rsv.Off; + _opcode = opcode; + _mask = mask; + + var len = payload.Length; + if (len < 126) + { + _payloadLength = (byte)len; + _extPayloadLength = new byte[0]; + } + else if (len < 0x010000) + { + _payloadLength = (byte)126; + _extPayloadLength = ((ushort)len).ToByteArrayInternally(ByteOrder.Big); + } + else + { + _payloadLength = (byte)127; + _extPayloadLength = len.ToByteArrayInternally(ByteOrder.Big); + } + + if (mask == Mask.Mask) + { + _maskingKey = createMaskingKey(); + payload.Mask(_maskingKey); + } + else + { + _maskingKey = new byte[0]; + } + + _payloadData = payload; + } + + #endregion + + #region Public Properties + + public byte[] ExtendedPayloadLength + { + get + { + return _extPayloadLength; + } + } + + public Fin Fin + { + get + { + return _fin; + } + } + + public bool IsBinary + { + get + { + return _opcode == Opcode.Binary; + } + } + + public bool IsClose + { + get + { + return _opcode == Opcode.Close; + } + } + + public bool IsCompressed + { + get + { + return _rsv1 == Rsv.On; + } + } + + public bool IsContinuation + { + get + { + return _opcode == Opcode.Cont; + } + } + + public bool IsControl + { + get + { + return _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong; + } + } + + public bool IsData + { + get + { + return _opcode == Opcode.Binary || _opcode == Opcode.Text; + } + } + + public bool IsFinal + { + get + { + return _fin == Fin.Final; + } + } + + public bool IsFragmented + { + get + { + return _fin == Fin.More || _opcode == Opcode.Cont; + } + } + + public bool IsMasked + { + get + { + return _mask == Mask.Mask; + } + } + + public bool IsPerMessageCompressed + { + get + { + return (_opcode == Opcode.Binary || _opcode == Opcode.Text) && _rsv1 == Rsv.On; + } + } + + public bool IsPing + { + get + { + return _opcode == Opcode.Ping; + } + } + + public bool IsPong + { + get + { + return _opcode == Opcode.Pong; + } + } + + public bool IsText + { + get + { + return _opcode == Opcode.Text; + } + } + + public ulong Length + { + get + { + return 2 + (ulong)(_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length; + } + } + + public Mask Mask + { + get + { + return _mask; + } + } + + public byte[] MaskingKey + { + get + { + return _maskingKey; + } + } + + public Opcode Opcode + { + get + { + return _opcode; + } + } + + public PayloadData PayloadData + { + get + { + return _payloadData; + } + } + + public byte PayloadLength + { + get + { + return _payloadLength; + } + } + + public Rsv Rsv1 + { + get + { + return _rsv1; + } + } + + public Rsv Rsv2 + { + get + { + return _rsv2; + } + } + + public Rsv Rsv3 + { + get + { + return _rsv3; + } + } + + #endregion + + #region Private Methods + + private byte[] createMaskingKey() + { + var key = new byte[4]; + var rand = new Random(); + rand.NextBytes(key); + + return key; + } + + private static bool isControl(Opcode opcode) + { + return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong; + } + + private static bool isData(Opcode opcode) + { + return opcode == Opcode.Text || opcode == Opcode.Binary; + } + + private static WebSocketFrame read(byte[] header, Stream stream, bool unmask) + { + /* Header */ + + // FIN + var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; + // RSV1 + var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + // Opcode + var opcode = (Opcode)(header[0] & 0x0f); + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.Mask : Mask.Unmask; + // Payload Length + var payloadLen = (byte)(header[1] & 0x7f); + + // Check if correct frame. + var incorrect = isControl(opcode) && fin == Fin.More + ? "A control frame is fragmented." + : !isData(opcode) && rsv1 == Rsv.On + ? "A non data frame is compressed." + : null; + + if (incorrect != null) + throw new WebSocketException(CloseStatusCode.IncorrectData, incorrect); + + // Check if consistent frame. + if (isControl(opcode) && payloadLen > 125) + throw new WebSocketException( + CloseStatusCode.InconsistentData, + "The length of payload data of a control frame is greater than 125 bytes."); + + var frame = new WebSocketFrame(); + frame._fin = fin; + frame._rsv1 = rsv1; + frame._rsv2 = rsv2; + frame._rsv3 = rsv3; + frame._opcode = opcode; + frame._mask = mask; + frame._payloadLength = payloadLen; + + /* Extended Payload Length */ + + var size = payloadLen < 126 + ? 0 + : payloadLen == 126 + ? 2 + : 8; + + var extPayloadLen = size > 0 ? stream.ReadBytes(size) : new byte[0]; + if (size > 0 && extPayloadLen.Length != size) + throw new WebSocketException( + "The 'Extended Payload Length' of a frame cannot be read from the data source."); + + frame._extPayloadLength = extPayloadLen; + + /* Masking Key */ + + var masked = mask == Mask.Mask; + var maskingKey = masked ? stream.ReadBytes(4) : new byte[0]; + if (masked && maskingKey.Length != 4) + throw new WebSocketException( + "The 'Masking Key' of a frame cannot be read from the data source."); + + frame._maskingKey = maskingKey; + + /* Payload Data */ + + ulong len = payloadLen < 126 + ? payloadLen + : payloadLen == 126 + ? extPayloadLen.ToUInt16(ByteOrder.Big) + : extPayloadLen.ToUInt64(ByteOrder.Big); + + byte[] data = null; + if (len > 0) + { + // Check if allowable payload data length. + if (payloadLen > 126 && len > PayloadData.MaxLength) + throw new WebSocketException( + CloseStatusCode.TooBig, + "The length of 'Payload Data' of a frame is greater than the allowable length."); + + data = payloadLen > 126 + ? stream.ReadBytes((long)len, 1024) + : stream.ReadBytes((int)len); + + //if (data.LongLength != (long)len) + // throw new WebSocketException( + // "The 'Payload Data' of a frame cannot be read from the data source."); + } + else + { + data = new byte[0]; + } + + var payload = new PayloadData(data, masked); + if (masked && unmask) + { + payload.Mask(maskingKey); + frame._mask = Mask.Unmask; + frame._maskingKey = new byte[0]; + } + + frame._payloadData = payload; + return frame; + } + + #endregion + + #region Internal Methods + + internal static WebSocketFrame CreateCloseFrame(Mask mask, byte[] data) + { + return new WebSocketFrame(Opcode.Close, mask, new PayloadData(data)); + } + + internal static WebSocketFrame CreateCloseFrame(Mask mask, PayloadData payload) + { + return new WebSocketFrame(Opcode.Close, mask, payload); + } + + internal static WebSocketFrame CreateCloseFrame(Mask mask, CloseStatusCode code, string reason) + { + return new WebSocketFrame( + Opcode.Close, mask, new PayloadData(((ushort)code).Append(reason))); + } + + internal static WebSocketFrame CreatePingFrame(Mask mask) + { + return new WebSocketFrame(Opcode.Ping, mask, new PayloadData()); + } + + internal static WebSocketFrame CreatePingFrame(Mask mask, byte[] data) + { + return new WebSocketFrame(Opcode.Ping, mask, new PayloadData(data)); + } + + internal static WebSocketFrame CreatePongFrame(Mask mask, PayloadData payload) + { + return new WebSocketFrame(Opcode.Pong, mask, payload); + } + + internal static WebSocketFrame CreateWebSocketFrame( + Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + { + return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed); + } + + internal static WebSocketFrame Read(Stream stream) + { + return Read(stream, true); + } + + internal static WebSocketFrame Read(Stream stream, bool unmask) + { + var header = stream.ReadBytes(2); + if (header.Length != 2) + throw new WebSocketException( + "The header part of a frame cannot be read from the data source."); + + return read(header, stream, unmask); + } + + internal static async void ReadAsync( + Stream stream, bool unmask, Action completed, Action error) + { + try + { + var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); + if (header.Length != 2) + throw new WebSocketException( + "The header part of a frame cannot be read from the data source."); + + var frame = read(header, stream, unmask); + if (completed != null) + completed(frame); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator() + { + foreach (var b in ToByteArray()) + yield return b; + } + + public void Print(bool dumped) + { + //Console.WriteLine(dumped ? dump(this) : print(this)); + } + + public byte[] ToByteArray() + { + using (var buff = new MemoryStream()) + { + var header = (int)_fin; + header = (header << 1) + (int)_rsv1; + header = (header << 1) + (int)_rsv2; + header = (header << 1) + (int)_rsv3; + header = (header << 4) + (int)_opcode; + header = (header << 1) + (int)_mask; + header = (header << 7) + (int)_payloadLength; + buff.Write(((ushort)header).ToByteArrayInternally(ByteOrder.Big), 0, 2); + + if (_payloadLength > 125) + buff.Write(_extPayloadLength, 0, _extPayloadLength.Length); + + if (_mask == Mask.Mask) + buff.Write(_maskingKey, 0, _maskingKey.Length); + + if (_payloadLength > 0) + { + var payload = _payloadData.ToByteArray(); + if (_payloadLength < 127) + buff.Write(payload, 0, payload.Length); + else + buff.WriteBytes(payload); + } + + return buff.ToArray(); + } + } + + public override string ToString() + { + return BitConverter.ToString(ToByteArray()); + } + + #endregion + + #region Explicitly Implemented Interface Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocketState.cs b/SocketHttpListener.Portable/WebSocketState.cs new file mode 100644 index 000000000..73b3a49dd --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketState.cs @@ -0,0 +1,35 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the state of the WebSocket connection. + /// + /// + /// The values of the state are defined in + /// The WebSocket + /// API. + /// + public enum WebSocketState : ushort + { + /// + /// Equivalent to numeric value 0. + /// Indicates that the connection has not yet been established. + /// + Connecting = 0, + /// + /// Equivalent to numeric value 1. + /// Indicates that the connection is established and the communication is possible. + /// + Open = 1, + /// + /// Equivalent to numeric value 2. + /// Indicates that the connection is going through the closing handshake or + /// the WebSocket.Close method has been invoked. + /// + Closing = 2, + /// + /// Equivalent to numeric value 3. + /// Indicates that the connection has been closed or couldn't be opened. + /// + Closed = 3 + } +} diff --git a/SocketHttpListener.Portable/packages.config b/SocketHttpListener.Portable/packages.config new file mode 100644 index 000000000..2aae715b5 --- /dev/null +++ b/SocketHttpListener.Portable/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/project.json b/SocketHttpListener.Portable/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/SocketHttpListener.Portable/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file -- cgit v1.2.3 From 65a1ef020b205b1676bd7dd70e7261a1fa29b7a2 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 19 Nov 2016 00:52:49 -0500 Subject: move sync repository to portable project --- Emby.Common.Implementations/BaseApplicationHost.cs | 1 - .../ScheduledTasks/TaskManager.cs | 26 +- .../Updates/GithubUpdater.cs | 269 ------ Emby.Server.Core/ApplicationHost.cs | 24 +- Emby.Server.Core/Browser/BrowserLauncher.cs | 75 -- Emby.Server.Core/Data/SqliteItemRepository.cs | 29 +- .../EntryPoints/ExternalPortForwarding.cs | 3 +- Emby.Server.Core/EntryPoints/StartupWizard.cs | 59 -- Emby.Server.Core/HttpServerFactory.cs | 6 +- Emby.Server.Core/Migrations/DbMigration.cs | 64 -- .../Migrations/UpdateLevelMigration.cs | 131 --- Emby.Server.Core/Sync/SyncRepository.cs | 976 --------------------- .../Activity/ActivityRepository.cs | 2 +- .../Browser/BrowserLauncher.cs | 73 ++ .../Data/BaseSqliteRepository.cs | 20 +- .../Data/CleanDatabaseScheduledTask.cs | 148 +--- .../Emby.Server.Implementations.csproj | 4 + .../EntryPoints/StartupWizard.cs | 59 ++ .../FileOrganization/OrganizerScheduledTask.cs | 2 +- .../HttpServer/HttpListenerHost.cs | 8 +- .../Migrations/UpdateLevelMigration.cs | 130 +++ .../Security/AuthenticationRepository.cs | 4 +- Emby.Server.Implementations/Sync/SyncRepository.cs | 770 ++++++++++++++++ Emby.Server.Implementations/TV/TVSeriesManager.cs | 4 - MediaBrowser.Api/StartupWizardService.cs | 1 - MediaBrowser.Common/MediaBrowser.Common.csproj | 1 + MediaBrowser.Common/Updates/GithubUpdater.cs | 269 ++++++ .../Entities/InternalItemsQuery.cs | 1 - MediaBrowser.Controller/Entities/TV/Series.cs | 4 - .../Configuration/ServerConfiguration.cs | 4 - MediaBrowser.Model/Tasks/ITaskManager.cs | 2 - MediaBrowser.Server.Mono/MonoAppHost.cs | 26 + MediaBrowser.Server.Mono/Native/MonoFileSystem.cs | 3 +- MediaBrowser.ServerApplication/MainStartup.cs | 2 +- MediaBrowser.ServerApplication/ServerNotifyIcon.cs | 2 +- MediaBrowser.ServerApplication/WindowsAppHost.cs | 9 + 36 files changed, 1392 insertions(+), 1819 deletions(-) delete mode 100644 Emby.Common.Implementations/Updates/GithubUpdater.cs delete mode 100644 Emby.Server.Core/Browser/BrowserLauncher.cs delete mode 100644 Emby.Server.Core/EntryPoints/StartupWizard.cs delete mode 100644 Emby.Server.Core/Migrations/DbMigration.cs delete mode 100644 Emby.Server.Core/Migrations/UpdateLevelMigration.cs delete mode 100644 Emby.Server.Core/Sync/SyncRepository.cs create mode 100644 Emby.Server.Implementations/Browser/BrowserLauncher.cs create mode 100644 Emby.Server.Implementations/EntryPoints/StartupWizard.cs create mode 100644 Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs create mode 100644 Emby.Server.Implementations/Sync/SyncRepository.cs create mode 100644 MediaBrowser.Common/Updates/GithubUpdater.cs (limited to 'Emby.Server.Implementations/FileOrganization') diff --git a/Emby.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs index 1f194968c..02d7cb31f 100644 --- a/Emby.Common.Implementations/BaseApplicationHost.cs +++ b/Emby.Common.Implementations/BaseApplicationHost.cs @@ -4,7 +4,6 @@ using Emby.Common.Implementations.Devices; using Emby.Common.Implementations.IO; using Emby.Common.Implementations.ScheduledTasks; using Emby.Common.Implementations.Serialization; -using Emby.Common.Implementations.Updates; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Progress; diff --git a/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs index 218af7ed5..b0153c588 100644 --- a/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs @@ -55,25 +55,6 @@ namespace Emby.Common.Implementations.ScheduledTasks private ILogger Logger { get; set; } private readonly IFileSystem _fileSystem; - private bool _suspendTriggers; - - public bool SuspendTriggers - { - get { return _suspendTriggers; } - set - { - Logger.Info("Setting SuspendTriggers to {0}", value); - var executeQueued = _suspendTriggers && !value; - - _suspendTriggers = value; - - if (executeQueued) - { - ExecuteQueuedTasks(); - } - } - } - /// /// Initializes a new instance of the class. /// @@ -230,7 +211,7 @@ namespace Emby.Common.Implementations.ScheduledTasks lock (_taskQueue) { - if (task.State == TaskState.Idle && !SuspendTriggers) + if (task.State == TaskState.Idle) { Execute(task, options); return; @@ -322,11 +303,6 @@ namespace Emby.Common.Implementations.ScheduledTasks /// private void ExecuteQueuedTasks() { - if (SuspendTriggers) - { - return; - } - Logger.Info("ExecuteQueuedTasks"); // Execute queued tasks diff --git a/Emby.Common.Implementations/Updates/GithubUpdater.cs b/Emby.Common.Implementations/Updates/GithubUpdater.cs deleted file mode 100644 index 42bc29ed5..000000000 --- a/Emby.Common.Implementations/Updates/GithubUpdater.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Updates; - -namespace Emby.Common.Implementations.Updates -{ - public class GithubUpdater - { - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _jsonSerializer; - - public GithubUpdater(IHttpClient httpClient, IJsonSerializer jsonSerializer) - { - _httpClient = httpClient; - _jsonSerializer = jsonSerializer; - } - - public async Task CheckForUpdateResult(string organzation, string repository, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename, TimeSpan cacheLength, CancellationToken cancellationToken) - { - var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); - - var options = new HttpRequestOptions - { - Url = url, - EnableKeepAlive = false, - CancellationToken = cancellationToken, - UserAgent = "Emby/3.0", - BufferContent = false - }; - - if (cacheLength.Ticks > 0) - { - options.CacheMode = CacheMode.Unconditional; - options.CacheLength = cacheLength; - } - - using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) - { - var obj = _jsonSerializer.DeserializeFromStream(stream); - - return CheckForUpdateResult(obj, minVersion, updateLevel, assetFilename, packageName, targetFilename); - } - } - - private CheckForUpdateResult CheckForUpdateResult(RootObject[] obj, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename) - { - if (updateLevel == PackageVersionClass.Release) - { - // Technically all we need to do is check that it's not pre-release - // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. - obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); - } - else if (updateLevel == PackageVersionClass.Beta) - { - obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase)).ToArray(); - } - else if (updateLevel == PackageVersionClass.Dev) - { - obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - var availableUpdate = obj - .Select(i => CheckForUpdateResult(i, minVersion, assetFilename, packageName, targetFilename)) - .Where(i => i != null) - .OrderByDescending(i => Version.Parse(i.AvailableVersion)) - .FirstOrDefault(); - - return availableUpdate ?? new CheckForUpdateResult - { - IsUpdateAvailable = false - }; - } - - private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel) - { - if (updateLevel == PackageVersionClass.Beta) - { - return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase); - } - if (updateLevel == PackageVersionClass.Dev) - { - return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || - i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); - } - - // Technically all we need to do is check that it's not pre-release - // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. - return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && - !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); - } - - public async Task> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken) - { - var list = new List(); - - var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); - - var options = new HttpRequestOptions - { - Url = url, - EnableKeepAlive = false, - CancellationToken = cancellationToken, - UserAgent = "Emby/3.0", - BufferContent = false - }; - - using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) - { - var obj = _jsonSerializer.DeserializeFromStream(stream); - - obj = obj.Where(i => (i.assets ?? new List()).Any(a => IsAsset(a, assetFilename))).ToArray(); - - list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1)); - list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1)); - list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1)); - - return list; - } - } - - public Version GetVersion(RootObject obj) - { - Version version; - if (!Version.TryParse(obj.tag_name, out version)) - { - return new Version(1, 0); - } - - return version; - } - - private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename) - { - Version version; - if (!Version.TryParse(obj.tag_name, out version)) - { - return null; - } - - if (version < minVersion) - { - return null; - } - - var asset = (obj.assets ?? new List()).FirstOrDefault(i => IsAsset(i, assetFilename)); - - if (asset == null) - { - return null; - } - - return new CheckForUpdateResult - { - AvailableVersion = version.ToString(), - IsUpdateAvailable = version > minVersion, - Package = new PackageVersionInfo - { - classification = obj.prerelease ? - (obj.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase) ? PackageVersionClass.Dev : PackageVersionClass.Beta) : - PackageVersionClass.Release, - name = packageName, - sourceUrl = asset.browser_download_url, - targetFilename = targetFilename, - versionStr = version.ToString(), - requiredVersionStr = "1.0.0", - description = obj.body, - infoUrl = obj.html_url - } - }; - } - - private bool IsAsset(Asset asset, string assetFilename) - { - var downloadFilename = Path.GetFileName(asset.browser_download_url) ?? string.Empty; - - if (downloadFilename.IndexOf(assetFilename, StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - return string.Equals(assetFilename, downloadFilename, StringComparison.OrdinalIgnoreCase); - } - - public class Uploader - { - public string login { get; set; } - public int id { get; set; } - public string avatar_url { get; set; } - public string gravatar_id { get; set; } - public string url { get; set; } - public string html_url { get; set; } - public string followers_url { get; set; } - public string following_url { get; set; } - public string gists_url { get; set; } - public string starred_url { get; set; } - public string subscriptions_url { get; set; } - public string organizations_url { get; set; } - public string repos_url { get; set; } - public string events_url { get; set; } - public string received_events_url { get; set; } - public string type { get; set; } - public bool site_admin { get; set; } - } - - public class Asset - { - public string url { get; set; } - public int id { get; set; } - public string name { get; set; } - public object label { get; set; } - public Uploader uploader { get; set; } - public string content_type { get; set; } - public string state { get; set; } - public int size { get; set; } - public int download_count { get; set; } - public string created_at { get; set; } - public string updated_at { get; set; } - public string browser_download_url { get; set; } - } - - public class Author - { - public string login { get; set; } - public int id { get; set; } - public string avatar_url { get; set; } - public string gravatar_id { get; set; } - public string url { get; set; } - public string html_url { get; set; } - public string followers_url { get; set; } - public string following_url { get; set; } - public string gists_url { get; set; } - public string starred_url { get; set; } - public string subscriptions_url { get; set; } - public string organizations_url { get; set; } - public string repos_url { get; set; } - public string events_url { get; set; } - public string received_events_url { get; set; } - public string type { get; set; } - public bool site_admin { get; set; } - } - - public class RootObject - { - public string url { get; set; } - public string assets_url { get; set; } - public string upload_url { get; set; } - public string html_url { get; set; } - public int id { get; set; } - public string tag_name { get; set; } - public string target_commitish { get; set; } - public string name { get; set; } - public bool draft { get; set; } - public Author author { get; set; } - public bool prerelease { get; set; } - public string created_at { get; set; } - public string published_at { get; set; } - public List assets { get; set; } - public string tarball_url { get; set; } - public string zipball_url { get; set; } - public string body { get; set; } - } - } -} diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 6073991b1..00b9b99e6 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -65,7 +65,6 @@ using Emby.Common.Implementations.Networking; using Emby.Common.Implementations.Reflection; using Emby.Common.Implementations.Serialization; using Emby.Common.Implementations.TextEncoding; -using Emby.Common.Implementations.Updates; using Emby.Common.Implementations.Xml; using Emby.Photos; using MediaBrowser.Model.IO; @@ -90,10 +89,10 @@ using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.FFMpeg; using Emby.Server.Core.IO; using Emby.Server.Core.Localization; -using Emby.Server.Core.Migrations; +using Emby.Server.Implementations.Migrations; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Social; -using Emby.Server.Core.Sync; +using Emby.Server.Implementations.Sync; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Connect; @@ -357,12 +356,6 @@ namespace Emby.Server.Core { await PerformPreInitMigrations().ConfigureAwait(false); - if (ServerConfigurationManager.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion && - ServerConfigurationManager.Configuration.IsStartupWizardCompleted) - { - TaskManager.SuspendTriggers = true; - } - await base.RunStartupTasks().ConfigureAwait(false); await MediaEncoder.Init().ConfigureAwait(false); @@ -494,7 +487,6 @@ namespace Emby.Server.Core { var migrations = new List { - new DbMigration(ServerConfigurationManager, TaskManager) }; foreach (var task in migrations) @@ -568,7 +560,7 @@ namespace Emby.Server.Core AuthenticationRepository = await GetAuthenticationRepository().ConfigureAwait(false); RegisterSingleInstance(AuthenticationRepository); - SyncRepository = await GetSyncRepository().ConfigureAwait(false); + SyncRepository = GetSyncRepository(); RegisterSingleInstance(SyncRepository); UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, _defaultUserNameFactory()); @@ -591,7 +583,7 @@ namespace Emby.Server.Core CertificatePath = GetCertificatePath(true); Certificate = GetCertificate(CertificatePath); - HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate); + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, SupportsDualModeSockets); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); RegisterSingleInstance(HttpServer, false); progress.Report(10); @@ -714,6 +706,8 @@ namespace Emby.Server.Core await ((UserManager)UserManager).Initialize().ConfigureAwait(false); } + protected abstract bool SupportsDualModeSockets { get; } + private ICertificate GetCertificate(string certificateLocation) { if (string.IsNullOrWhiteSpace(certificateLocation)) @@ -844,11 +838,11 @@ namespace Emby.Server.Core return repo; } - private async Task GetSyncRepository() + private ISyncRepository GetSyncRepository() { - var repo = new SyncRepository(LogManager, JsonSerializer, ServerConfigurationManager.ApplicationPaths, GetDbConnector()); + var repo = new SyncRepository(LogManager.GetLogger("SyncRepository"), JsonSerializer, ServerConfigurationManager.ApplicationPaths); - await repo.Initialize().ConfigureAwait(false); + repo.Initialize(); return repo; } diff --git a/Emby.Server.Core/Browser/BrowserLauncher.cs b/Emby.Server.Core/Browser/BrowserLauncher.cs deleted file mode 100644 index 4ccc41c30..000000000 --- a/Emby.Server.Core/Browser/BrowserLauncher.cs +++ /dev/null @@ -1,75 +0,0 @@ -using MediaBrowser.Controller; -using System; - -namespace Emby.Server.Core.Browser -{ - /// - /// Class BrowserLauncher - /// - public static class BrowserLauncher - { - /// - /// Opens the dashboard page. - /// - /// The page. - /// The app host. - public static void OpenDashboardPage(string page, IServerApplicationHost appHost) - { - var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; - - OpenUrl(appHost, url); - } - - /// - /// Opens the community. - /// - public static void OpenCommunity(IServerApplicationHost appHost) - { - OpenUrl(appHost, "http://emby.media/community"); - } - - public static void OpenEmbyPremiere(IServerApplicationHost appHost) - { - OpenDashboardPage("supporterkey.html", appHost); - } - - /// - /// Opens the web client. - /// - /// The app host. - public static void OpenWebClient(IServerApplicationHost appHost) - { - OpenDashboardPage("index.html", appHost); - } - - /// - /// Opens the dashboard. - /// - /// The app host. - public static void OpenDashboard(IServerApplicationHost appHost) - { - OpenDashboardPage("dashboard.html", appHost); - } - - /// - /// Opens the URL. - /// - /// The URL. - private static void OpenUrl(IServerApplicationHost appHost, string url) - { - try - { - appHost.LaunchUrl(url); - } - catch (NotImplementedException) - { - - } - catch (Exception ex) - { - Console.WriteLine("Error launching url: " + url); - Console.WriteLine(ex.Message); - } - } - } -} diff --git a/Emby.Server.Core/Data/SqliteItemRepository.cs b/Emby.Server.Core/Data/SqliteItemRepository.cs index ed03c0f67..2f08081f6 100644 --- a/Emby.Server.Core/Data/SqliteItemRepository.cs +++ b/Emby.Server.Core/Data/SqliteItemRepository.cs @@ -3060,18 +3060,6 @@ namespace Emby.Server.Core.Data { //whereClauses.Add("(UserId is null or UserId=@UserId)"); } - if (query.IsCurrentSchema.HasValue) - { - if (query.IsCurrentSchema.Value) - { - whereClauses.Add("(SchemaVersion not null AND SchemaVersion=@SchemaVersion)"); - } - else - { - whereClauses.Add("(SchemaVersion is null or SchemaVersion<>@SchemaVersion)"); - } - cmd.Parameters.Add(cmd, "@SchemaVersion", DbType.Int32).Value = LatestSchemaVersion; - } if (query.IsHD.HasValue) { whereClauses.Add("IsHD=@IsHD"); @@ -3454,7 +3442,7 @@ namespace Emby.Server.Core.Data cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower(); } - if (query.ImageTypes.Length > 0 && _config.Configuration.SchemaVersion >= 87) + if (query.ImageTypes.Length > 0) { foreach (var requiredImage in query.ImageTypes) { @@ -3738,15 +3726,8 @@ namespace Emby.Server.Core.Data } if (query.IsVirtualItem.HasValue) { - if (_config.Configuration.SchemaVersion >= 90) - { - whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; - } - else if (!query.IsVirtualItem.Value) - { - whereClauses.Add("LocationType<>'Virtual'"); - } + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; } if (query.IsSpecialSeason.HasValue) { @@ -3770,7 +3751,7 @@ namespace Emby.Server.Core.Data whereClauses.Add("PremiereDate < DATETIME('now')"); } } - if (query.IsMissing.HasValue && _config.Configuration.SchemaVersion >= 90) + if (query.IsMissing.HasValue) { if (query.IsMissing.Value) { @@ -3781,7 +3762,7 @@ namespace Emby.Server.Core.Data whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))"); } } - if (query.IsVirtualUnaired.HasValue && _config.Configuration.SchemaVersion >= 90) + if (query.IsVirtualUnaired.HasValue) { if (query.IsVirtualUnaired.Value) { diff --git a/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs index c1a19a71f..11e275940 100644 --- a/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs @@ -95,7 +95,7 @@ namespace Emby.Server.Core.EntryPoints NatUtility.StartDiscovery(); - _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; @@ -233,6 +233,7 @@ namespace Emby.Server.Core.EntryPoints await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) { Description = _appHost.Name + }).ConfigureAwait(false); } catch (Exception ex) diff --git a/Emby.Server.Core/EntryPoints/StartupWizard.cs b/Emby.Server.Core/EntryPoints/StartupWizard.cs deleted file mode 100644 index 30ceca073..000000000 --- a/Emby.Server.Core/EntryPoints/StartupWizard.cs +++ /dev/null @@ -1,59 +0,0 @@ -using MediaBrowser.Controller; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Logging; -using Emby.Server.Core.Browser; - -namespace Emby.Server.Core.EntryPoints -{ - /// - /// Class StartupWizard - /// - public class StartupWizard : IServerEntryPoint - { - /// - /// The _app host - /// - private readonly IServerApplicationHost _appHost; - /// - /// The _user manager - /// - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The app host. - /// The logger. - public StartupWizard(IServerApplicationHost appHost, ILogger logger) - { - _appHost = appHost; - _logger = logger; - } - - /// - /// Runs this instance. - /// - public void Run() - { - if (_appHost.IsFirstRun) - { - LaunchStartupWizard(); - } - } - - /// - /// Launches the startup wizard. - /// - private void LaunchStartupWizard() - { - BrowserLauncher.OpenDashboardPage("wizardstart.html", _appHost); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - } - } -} \ No newline at end of file diff --git a/Emby.Server.Core/HttpServerFactory.cs b/Emby.Server.Core/HttpServerFactory.cs index d6c07e258..8ec5fb026 100644 --- a/Emby.Server.Core/HttpServerFactory.cs +++ b/Emby.Server.Core/HttpServerFactory.cs @@ -44,7 +44,8 @@ namespace Emby.Server.Core IJsonSerializer json, IXmlSerializer xml, IEnvironmentInfo environment, - ICertificate certificate) + ICertificate certificate, + bool enableDualModeSockets) { var logger = logManager.GetLogger("HttpServer"); @@ -63,7 +64,8 @@ namespace Emby.Server.Core environment, certificate, new StreamFactory(), - GetParseFn); + GetParseFn, + enableDualModeSockets); } private static Func GetParseFn(Type propertyType) diff --git a/Emby.Server.Core/Migrations/DbMigration.cs b/Emby.Server.Core/Migrations/DbMigration.cs deleted file mode 100644 index 5d652770f..000000000 --- a/Emby.Server.Core/Migrations/DbMigration.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Threading.Tasks; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Tasks; -using Emby.Server.Core.Data; -using Emby.Server.Implementations.Migrations; - -namespace Emby.Server.Core.Migrations -{ - public class DbMigration : IVersionMigration - { - private readonly IServerConfigurationManager _config; - private readonly ITaskManager _taskManager; - - public DbMigration(IServerConfigurationManager config, ITaskManager taskManager) - { - _config = config; - _taskManager = taskManager; - } - - public async Task Run() - { - // If a forced migration is required, do that now - if (_config.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion) - { - if (!_config.Configuration.IsStartupWizardCompleted) - { - _config.Configuration.MigrationVersion = CleanDatabaseScheduledTask.MigrationVersion; - _config.SaveConfiguration(); - return; - } - - _taskManager.SuspendTriggers = true; - CleanDatabaseScheduledTask.EnableUnavailableMessage = true; - - Task.Run(async () => - { - await Task.Delay(1000).ConfigureAwait(false); - - _taskManager.Execute(); - }); - - return; - } - - if (_config.Configuration.SchemaVersion < SqliteItemRepository.LatestSchemaVersion) - { - if (!_config.Configuration.IsStartupWizardCompleted) - { - _config.Configuration.SchemaVersion = SqliteItemRepository.LatestSchemaVersion; - _config.SaveConfiguration(); - return; - } - - Task.Run(async () => - { - await Task.Delay(1000).ConfigureAwait(false); - - _taskManager.Execute(); - }); - } - } - } -} diff --git a/Emby.Server.Core/Migrations/UpdateLevelMigration.cs b/Emby.Server.Core/Migrations/UpdateLevelMigration.cs deleted file mode 100644 index c79dbabea..000000000 --- a/Emby.Server.Core/Migrations/UpdateLevelMigration.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Emby.Common.Implementations.Updates; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Updates; -using Emby.Server.Implementations.Migrations; - -namespace Emby.Server.Core.Migrations -{ - public class UpdateLevelMigration : IVersionMigration - { - private readonly IServerConfigurationManager _config; - private readonly IServerApplicationHost _appHost; - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _jsonSerializer; - private readonly string _releaseAssetFilename; - private readonly ILogger _logger; - - public UpdateLevelMigration(IServerConfigurationManager config, IServerApplicationHost appHost, IHttpClient httpClient, IJsonSerializer jsonSerializer, string releaseAssetFilename, ILogger logger) - { - _config = config; - _appHost = appHost; - _httpClient = httpClient; - _jsonSerializer = jsonSerializer; - _releaseAssetFilename = releaseAssetFilename; - _logger = logger; - } - - public async Task Run() - { - var lastVersion = _config.Configuration.LastVersion; - var currentVersion = _appHost.ApplicationVersion; - - if (string.Equals(lastVersion, currentVersion.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return; - } - - try - { - var updateLevel = _config.Configuration.SystemUpdateLevel; - - await CheckVersion(currentVersion, updateLevel, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error in update migration", ex); - } - } - - private async Task CheckVersion(Version currentVersion, PackageVersionClass currentUpdateLevel, CancellationToken cancellationToken) - { - var releases = await new GithubUpdater(_httpClient, _jsonSerializer) - .GetLatestReleases("MediaBrowser", "Emby", _releaseAssetFilename, cancellationToken).ConfigureAwait(false); - - var newUpdateLevel = GetNewUpdateLevel(currentVersion, currentUpdateLevel, releases); - - if (newUpdateLevel != currentUpdateLevel) - { - _config.Configuration.SystemUpdateLevel = newUpdateLevel; - _config.SaveConfiguration(); - } - } - - private PackageVersionClass GetNewUpdateLevel(Version currentVersion, PackageVersionClass currentUpdateLevel, List releases) - { - var newUpdateLevel = currentUpdateLevel; - - // If the current version is later than current stable, set the update level to beta - if (releases.Count >= 1) - { - var release = releases[0]; - var version = ParseVersion(release.tag_name); - if (version != null) - { - if (currentVersion > version) - { - newUpdateLevel = PackageVersionClass.Beta; - } - else - { - return PackageVersionClass.Release; - } - } - } - - // If the current version is later than current beta, set the update level to dev - if (releases.Count >= 2) - { - var release = releases[1]; - var version = ParseVersion(release.tag_name); - if (version != null) - { - if (currentVersion > version) - { - newUpdateLevel = PackageVersionClass.Dev; - } - else - { - return PackageVersionClass.Beta; - } - } - } - - return newUpdateLevel; - } - - private Version ParseVersion(string versionString) - { - if (!string.IsNullOrWhiteSpace(versionString)) - { - var parts = versionString.Split('.'); - if (parts.Length == 3) - { - versionString += ".0"; - } - } - - Version version; - Version.TryParse(versionString, out version); - - return version; - } - } -} diff --git a/Emby.Server.Core/Sync/SyncRepository.cs b/Emby.Server.Core/Sync/SyncRepository.cs deleted file mode 100644 index bfcf76767..000000000 --- a/Emby.Server.Core/Sync/SyncRepository.cs +++ /dev/null @@ -1,976 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Emby.Server.Core.Data; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Sync; - -namespace Emby.Server.Core.Sync -{ - public class SyncRepository : BaseSqliteRepository, ISyncRepository - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private readonly IJsonSerializer _json; - - public SyncRepository(ILogManager logManager, IJsonSerializer json, IServerApplicationPaths appPaths, IDbConnector connector) - : base(logManager, connector) - { - _json = json; - DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db"); - } - - private class SyncSummary - { - public Dictionary Items { get; set; } - - public SyncSummary() - { - Items = new Dictionary(); - } - } - - - public async Task Initialize() - { - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - string[] queries = { - - "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", - - "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)", - - "drop index if exists idx_SyncJobItems2", - "drop index if exists idx_SyncJobItems3", - "drop index if exists idx_SyncJobs1", - "drop index if exists idx_SyncJobs", - "drop index if exists idx_SyncJobItems1", - "create index if not exists idx_SyncJobItems4 on SyncJobItems(TargetId,ItemId,Status,Progress,DateCreated)", - "create index if not exists idx_SyncJobItems5 on SyncJobItems(TargetId,Status,ItemId,Progress)", - - "create index if not exists idx_SyncJobs2 on SyncJobs(TargetId,Status,ItemIds,Progress)", - - "pragma shrink_memory" - }; - - connection.RunQueries(queries, Logger); - - connection.AddColumn(Logger, "SyncJobs", "Profile", "TEXT"); - connection.AddColumn(Logger, "SyncJobs", "Bitrate", "INT"); - connection.AddColumn(Logger, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT"); - } - } - - private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; - private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; - - public SyncJob GetJob(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - CheckDisposed(); - - var guid = new Guid(id); - - if (guid == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseJobSelectText + " where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - return GetJob(reader); - } - } - } - - return null; - } - } - - private SyncJob GetJob(IDataReader reader) - { - var info = new SyncJob - { - Id = reader.GetGuid(0).ToString("N"), - TargetId = reader.GetString(1), - Name = reader.GetString(2) - }; - - if (!reader.IsDBNull(3)) - { - info.Profile = reader.GetString(3); - } - - if (!reader.IsDBNull(4)) - { - info.Quality = reader.GetString(4); - } - - if (!reader.IsDBNull(5)) - { - info.Bitrate = reader.GetInt32(5); - } - - if (!reader.IsDBNull(6)) - { - info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader.GetString(6), true); - } - - if (!reader.IsDBNull(7)) - { - info.Progress = reader.GetDouble(7); - } - - if (!reader.IsDBNull(8)) - { - info.UserId = reader.GetString(8); - } - - if (!reader.IsDBNull(9)) - { - info.RequestedItemIds = reader.GetString(9).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - - if (!reader.IsDBNull(10)) - { - info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader.GetString(10), true); - } - - if (!reader.IsDBNull(11)) - { - info.ParentId = reader.GetString(11); - } - - if (!reader.IsDBNull(12)) - { - info.UnwatchedOnly = reader.GetBoolean(12); - } - - if (!reader.IsDBNull(13)) - { - info.ItemLimit = reader.GetInt32(13); - } - - info.SyncNewContent = reader.GetBoolean(14); - - info.DateCreated = reader.GetDateTime(15).ToUniversalTime(); - info.DateLastModified = reader.GetDateTime(16).ToUniversalTime(); - info.ItemCount = reader.GetInt32(17); - - return info; - } - - public Task Create(SyncJob job) - { - return InsertOrUpdate(job, true); - } - - public Task Update(SyncJob job) - { - return InsertOrUpdate(job, false); - } - - private async Task InsertOrUpdate(SyncJob job, bool insert) - { - if (job == null) - { - throw new ArgumentNullException("job"); - } - - CheckDisposed(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var cmd = connection.CreateCommand()) - { - if (insert) - { - cmd.CommandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@Name"); - cmd.Parameters.Add(cmd, "@Profile"); - cmd.Parameters.Add(cmd, "@Quality"); - cmd.Parameters.Add(cmd, "@Bitrate"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@UserId"); - cmd.Parameters.Add(cmd, "@ItemIds"); - cmd.Parameters.Add(cmd, "@Category"); - cmd.Parameters.Add(cmd, "@ParentId"); - cmd.Parameters.Add(cmd, "@UnwatchedOnly"); - cmd.Parameters.Add(cmd, "@ItemLimit"); - cmd.Parameters.Add(cmd, "@SyncNewContent"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@DateLastModified"); - cmd.Parameters.Add(cmd, "@ItemCount"); - } - else - { - cmd.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@Name"); - cmd.Parameters.Add(cmd, "@Profile"); - cmd.Parameters.Add(cmd, "@Quality"); - cmd.Parameters.Add(cmd, "@Bitrate"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@UserId"); - cmd.Parameters.Add(cmd, "@ItemIds"); - cmd.Parameters.Add(cmd, "@Category"); - cmd.Parameters.Add(cmd, "@ParentId"); - cmd.Parameters.Add(cmd, "@UnwatchedOnly"); - cmd.Parameters.Add(cmd, "@ItemLimit"); - cmd.Parameters.Add(cmd, "@SyncNewContent"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@DateLastModified"); - cmd.Parameters.Add(cmd, "@ItemCount"); - } - - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - var index = 0; - - cmd.GetParameter(index++).Value = new Guid(job.Id); - cmd.GetParameter(index++).Value = job.TargetId; - cmd.GetParameter(index++).Value = job.Name; - cmd.GetParameter(index++).Value = job.Profile; - cmd.GetParameter(index++).Value = job.Quality; - cmd.GetParameter(index++).Value = job.Bitrate; - cmd.GetParameter(index++).Value = job.Status.ToString(); - cmd.GetParameter(index++).Value = job.Progress; - cmd.GetParameter(index++).Value = job.UserId; - cmd.GetParameter(index++).Value = string.Join(",", job.RequestedItemIds.ToArray()); - cmd.GetParameter(index++).Value = job.Category; - cmd.GetParameter(index++).Value = job.ParentId; - cmd.GetParameter(index++).Value = job.UnwatchedOnly; - cmd.GetParameter(index++).Value = job.ItemLimit; - cmd.GetParameter(index++).Value = job.SyncNewContent; - cmd.GetParameter(index++).Value = job.DateCreated; - cmd.GetParameter(index++).Value = job.DateLastModified; - cmd.GetParameter(index++).Value = job.ItemCount; - - cmd.Transaction = transaction; - - cmd.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - - public async Task DeleteJob(string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException("id"); - } - - CheckDisposed(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var deleteJobCommand = connection.CreateCommand()) - { - using (var deleteJobItemsCommand = connection.CreateCommand()) - { - IDbTransaction transaction = null; - - try - { - // _deleteJobCommand - deleteJobCommand.CommandText = "delete from SyncJobs where Id=@Id"; - deleteJobCommand.Parameters.Add(deleteJobCommand, "@Id"); - - transaction = connection.BeginTransaction(); - - deleteJobCommand.GetParameter(0).Value = new Guid(id); - deleteJobCommand.Transaction = transaction; - deleteJobCommand.ExecuteNonQuery(); - - // _deleteJobItemsCommand - deleteJobItemsCommand.CommandText = "delete from SyncJobItems where JobId=@JobId"; - deleteJobItemsCommand.Parameters.Add(deleteJobItemsCommand, "@JobId"); - - deleteJobItemsCommand.GetParameter(0).Value = id; - deleteJobItemsCommand.Transaction = transaction; - deleteJobItemsCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - } - - public QueryResult GetJobs(SyncJobQuery query) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - CheckDisposed(); - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseJobSelectText; - - var whereClauses = new List(); - - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - if (!string.IsNullOrWhiteSpace(query.ExcludeTargetIds)) - { - var excludeIds = (query.ExcludeTargetIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - if (excludeIds.Length == 1) - { - whereClauses.Add("TargetId<>@ExcludeTargetId"); - cmd.Parameters.Add(cmd, "@ExcludeTargetId", DbType.String).Value = excludeIds[0]; - } - else if (excludeIds.Length > 1) - { - whereClauses.Add("TargetId<>@ExcludeTargetId"); - cmd.Parameters.Add(cmd, "@ExcludeTargetId", DbType.String).Value = excludeIds[0]; - } - } - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - whereClauses.Add("UserId=@UserId"); - cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; - } - if (query.SyncNewContent.HasValue) - { - whereClauses.Add("SyncNewContent=@SyncNewContent"); - cmd.Parameters.Add(cmd, "@SyncNewContent", DbType.Boolean).Value = query.SyncNewContent.Value; - } - - cmd.CommandText += " mainTable"; - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - var startIndex = query.StartIndex ?? 0; - if (startIndex > 0) - { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", - startIndex.ToString(_usCulture))); - } - - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; - - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } - - cmd.CommandText += "; select count (Id) from SyncJobs" + whereTextWithoutPaging; - - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) - { - list.Add(GetJob(reader)); - } - - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } - } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - public SyncJobItem GetJobItem(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - CheckDisposed(); - - var guid = new Guid(id); - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseJobItemSelectText + " where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - return GetJobItem(reader); - } - } - } - - return null; - } - } - - private QueryResult GetJobItemReader(SyncJobItemQuery query, string baseSelectText, Func itemFactory) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = baseSelectText; - - var whereClauses = new List(); - - if (!string.IsNullOrWhiteSpace(query.JobId)) - { - whereClauses.Add("JobId=@JobId"); - cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; - } - if (!string.IsNullOrWhiteSpace(query.ItemId)) - { - whereClauses.Add("ItemId=@ItemId"); - cmd.Parameters.Add(cmd, "@ItemId", DbType.String).Value = query.ItemId; - } - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - var startIndex = query.StartIndex ?? 0; - if (startIndex > 0) - { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", - startIndex.ToString(_usCulture))); - } - - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += " ORDER BY JobItemIndex, DateCreated"; - - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } - - cmd.CommandText += "; select count (Id) from SyncJobItems" + whereTextWithoutPaging; - - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) - { - list.Add(itemFactory(reader)); - } - - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } - } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) - { - var result = new Dictionary(); - - var now = DateTime.UtcNow; - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "select ItemId,Status,Progress from SyncJobItems"; - - var whereClauses = new List(); - - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += ";" + cmd.CommandText - .Replace("select ItemId,Status,Progress from SyncJobItems", "select ItemIds,Status,Progress from SyncJobs") - .Replace("'Synced'", "'Completed','CompletedWithError'"); - - //Logger.Debug(cmd.CommandText); - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - LogQueryTime("GetSyncedItemProgresses", cmd, now); - - while (reader.Read()) - { - AddStatusResult(reader, result, false); - } - - if (reader.NextResult()) - { - while (reader.Read()) - { - AddStatusResult(reader, result, true); - } - } - } - } - } - - return result; - } - - private void LogQueryTime(string methodName, IDbCommand cmd, DateTime startDate) - { - var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; - - var slowThreshold = 1000; - -#if DEBUG - slowThreshold = 50; -#endif - - if (elapsed >= slowThreshold) - { - Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", - Convert.ToInt32(elapsed), - cmd.CommandText, - methodName); - } - else - { - //Logger.Debug("{2} query time: {0}ms. Query: {1}", - // Convert.ToInt32(elapsed), - // cmd.CommandText, - // methodName); - } - } - - private void AddStatusResult(IDataReader reader, Dictionary result, bool multipleIds) - { - if (reader.IsDBNull(0)) - { - return; - } - - var itemIds = new List(); - - var ids = reader.GetString(0); - - if (multipleIds) - { - itemIds = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - else - { - itemIds.Add(ids); - } - - if (!reader.IsDBNull(1)) - { - SyncJobItemStatus status; - var statusString = reader.GetString(1); - if (string.Equals(statusString, "Completed", StringComparison.OrdinalIgnoreCase) || - string.Equals(statusString, "CompletedWithError", StringComparison.OrdinalIgnoreCase)) - { - status = SyncJobItemStatus.Synced; - } - else - { - status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), statusString, true); - } - - if (status == SyncJobItemStatus.Synced) - { - foreach (var itemId in itemIds) - { - result[itemId] = new SyncedItemProgress - { - Status = SyncJobItemStatus.Synced - }; - } - } - else - { - double progress = reader.IsDBNull(2) ? 0.0 : reader.GetDouble(2); - - foreach (var itemId in itemIds) - { - SyncedItemProgress currentStatus; - if (!result.TryGetValue(itemId, out currentStatus) || (currentStatus.Status != SyncJobItemStatus.Synced && progress >= currentStatus.Progress)) - { - result[itemId] = new SyncedItemProgress - { - Status = status, - Progress = progress - }; - } - } - } - } - } - - public QueryResult GetJobItems(SyncJobItemQuery query) - { - return GetJobItemReader(query, BaseJobItemSelectText, GetJobItem); - } - - public Task Create(SyncJobItem jobItem) - { - return InsertOrUpdate(jobItem, true); - } - - public Task Update(SyncJobItem jobItem) - { - return InsertOrUpdate(jobItem, false); - } - - private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert) - { - if (jobItem == null) - { - throw new ArgumentNullException("jobItem"); - } - - CheckDisposed(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var cmd = connection.CreateCommand()) - { - if (insert) - { - cmd.CommandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (@Id, @ItemId, @ItemName, @MediaSourceId, @JobId, @TemporaryPath, @OutputPath, @Status, @TargetId, @DateCreated, @Progress, @AdditionalFiles, @MediaSource, @IsMarkedForRemoval, @JobItemIndex, @ItemDateModifiedTicks)"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@ItemId"); - cmd.Parameters.Add(cmd, "@ItemName"); - cmd.Parameters.Add(cmd, "@MediaSourceId"); - cmd.Parameters.Add(cmd, "@JobId"); - cmd.Parameters.Add(cmd, "@TemporaryPath"); - cmd.Parameters.Add(cmd, "@OutputPath"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@AdditionalFiles"); - cmd.Parameters.Add(cmd, "@MediaSource"); - cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); - cmd.Parameters.Add(cmd, "@JobItemIndex"); - cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); - } - else - { - // cmd - cmd.CommandText = "update SyncJobItems set ItemId=@ItemId,ItemName=@ItemName,MediaSourceId=@MediaSourceId,JobId=@JobId,TemporaryPath=@TemporaryPath,OutputPath=@OutputPath,Status=@Status,TargetId=@TargetId,DateCreated=@DateCreated,Progress=@Progress,AdditionalFiles=@AdditionalFiles,MediaSource=@MediaSource,IsMarkedForRemoval=@IsMarkedForRemoval,JobItemIndex=@JobItemIndex,ItemDateModifiedTicks=@ItemDateModifiedTicks where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@ItemId"); - cmd.Parameters.Add(cmd, "@ItemName"); - cmd.Parameters.Add(cmd, "@MediaSourceId"); - cmd.Parameters.Add(cmd, "@JobId"); - cmd.Parameters.Add(cmd, "@TemporaryPath"); - cmd.Parameters.Add(cmd, "@OutputPath"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@AdditionalFiles"); - cmd.Parameters.Add(cmd, "@MediaSource"); - cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); - cmd.Parameters.Add(cmd, "@JobItemIndex"); - cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); - } - - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - var index = 0; - - cmd.GetParameter(index++).Value = new Guid(jobItem.Id); - cmd.GetParameter(index++).Value = jobItem.ItemId; - cmd.GetParameter(index++).Value = jobItem.ItemName; - cmd.GetParameter(index++).Value = jobItem.MediaSourceId; - cmd.GetParameter(index++).Value = jobItem.JobId; - cmd.GetParameter(index++).Value = jobItem.TemporaryPath; - cmd.GetParameter(index++).Value = jobItem.OutputPath; - cmd.GetParameter(index++).Value = jobItem.Status.ToString(); - cmd.GetParameter(index++).Value = jobItem.TargetId; - cmd.GetParameter(index++).Value = jobItem.DateCreated; - cmd.GetParameter(index++).Value = jobItem.Progress; - cmd.GetParameter(index++).Value = _json.SerializeToString(jobItem.AdditionalFiles); - cmd.GetParameter(index++).Value = jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource); - cmd.GetParameter(index++).Value = jobItem.IsMarkedForRemoval; - cmd.GetParameter(index++).Value = jobItem.JobItemIndex; - cmd.GetParameter(index++).Value = jobItem.ItemDateModifiedTicks; - - cmd.Transaction = transaction; - - cmd.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - - private SyncJobItem GetJobItem(IDataReader reader) - { - var info = new SyncJobItem - { - Id = reader.GetGuid(0).ToString("N"), - ItemId = reader.GetString(1) - }; - - if (!reader.IsDBNull(2)) - { - info.ItemName = reader.GetString(2); - } - - if (!reader.IsDBNull(3)) - { - info.MediaSourceId = reader.GetString(3); - } - - info.JobId = reader.GetString(4); - - if (!reader.IsDBNull(5)) - { - info.TemporaryPath = reader.GetString(5); - } - if (!reader.IsDBNull(6)) - { - info.OutputPath = reader.GetString(6); - } - - if (!reader.IsDBNull(7)) - { - info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader.GetString(7), true); - } - - info.TargetId = reader.GetString(8); - - info.DateCreated = reader.GetDateTime(9).ToUniversalTime(); - - if (!reader.IsDBNull(10)) - { - info.Progress = reader.GetDouble(10); - } - - if (!reader.IsDBNull(11)) - { - var json = reader.GetString(11); - - if (!string.IsNullOrWhiteSpace(json)) - { - info.AdditionalFiles = _json.DeserializeFromString>(json); - } - } - - if (!reader.IsDBNull(12)) - { - var json = reader.GetString(12); - - if (!string.IsNullOrWhiteSpace(json)) - { - info.MediaSource = _json.DeserializeFromString(json); - } - } - - info.IsMarkedForRemoval = reader.GetBoolean(13); - info.JobItemIndex = reader.GetInt32(14); - - if (!reader.IsDBNull(15)) - { - info.ItemDateModifiedTicks = reader.GetInt64(15); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index ea9e537c9..7bc77402e 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.Activity if (minDate.HasValue) { - whereClauses.Add("DateCreated>=@DateCreated"); + whereClauses.Add("DateCreated>=?"); paramList.Add(minDate.Value.ToDateTimeParamValue()); } diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs new file mode 100644 index 000000000..05cde91e2 --- /dev/null +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -0,0 +1,73 @@ +using MediaBrowser.Controller; +using System; + +namespace Emby.Server.Implementations.Browser +{ + /// + /// Class BrowserLauncher + /// + public static class BrowserLauncher + { + /// + /// Opens the dashboard page. + /// + /// The page. + /// The app host. + public static void OpenDashboardPage(string page, IServerApplicationHost appHost) + { + var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; + + OpenUrl(appHost, url); + } + + /// + /// Opens the community. + /// + public static void OpenCommunity(IServerApplicationHost appHost) + { + OpenUrl(appHost, "http://emby.media/community"); + } + + public static void OpenEmbyPremiere(IServerApplicationHost appHost) + { + OpenDashboardPage("supporterkey.html", appHost); + } + + /// + /// Opens the web client. + /// + /// The app host. + public static void OpenWebClient(IServerApplicationHost appHost) + { + OpenDashboardPage("index.html", appHost); + } + + /// + /// Opens the dashboard. + /// + /// The app host. + public static void OpenDashboard(IServerApplicationHost appHost) + { + OpenDashboardPage("dashboard.html", appHost); + } + + /// + /// Opens the URL. + /// + /// The URL. + private static void OpenUrl(IServerApplicationHost appHost, string url) + { + try + { + appHost.LaunchUrl(url); + } + catch (NotImplementedException) + { + + } + catch (Exception) + { + } + } + } +} diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 8febe83b2..c47a534d1 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Logging; using SQLitePCL.pretty; +using System.Linq; namespace Emby.Server.Implementations.Data { @@ -120,21 +121,30 @@ namespace Emby.Server.Implementations.Data } - protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type) + protected List GetColumnNames(IDatabaseConnection connection, string table) { + var list = new List(); + foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) { if (row[1].SQLiteType != SQLiteType.Null) { var name = row[1].ToString(); - if (string.Equals(name, columnName, StringComparison.OrdinalIgnoreCase)) - { - return; - } + list.Add(name); } } + return list; + } + + protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List existingColumnNames) + { + if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase)) + { + return; + } + connection.ExecuteAll(string.Join(";", new string[] { "alter table " + table, diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index dd32e2cbd..3f11b6eb0 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -7,20 +6,13 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.IO; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.IO; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; -using Emby.Server.Implementations.ScheduledTasks; namespace Emby.Server.Implementations.Data { @@ -29,26 +21,14 @@ namespace Emby.Server.Implementations.Data private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly IHttpServer _httpServer; - private readonly ILocalizationManager _localization; - private readonly ITaskManager _taskManager; - public const int MigrationVersion = 23; - public static bool EnableUnavailableMessage = false; - const int LatestSchemaVersion = 109; - - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; - _config = config; _fileSystem = fileSystem; - _httpServer = httpServer; - _localization = localization; - _taskManager = taskManager; } public string Name @@ -68,8 +48,6 @@ namespace Emby.Server.Implementations.Data public async Task Execute(CancellationToken cancellationToken, IProgress progress) { - OnProgress(0); - // Ensure these objects are lazy loaded. // Without this there is a deadlock that will need to be investigated var rootChildren = _libraryManager.RootFolder.Children.ToList(); @@ -78,19 +56,7 @@ namespace Emby.Server.Implementations.Data var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(p => { - double newPercentCommplete = .4 * p; - OnProgress(newPercentCommplete); - - progress.Report(newPercentCommplete); - }); - - await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); - - innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(p => - { - double newPercentCommplete = 40 + .05 * p; - OnProgress(newPercentCommplete); + double newPercentCommplete = .45 * p; progress.Report(newPercentCommplete); }); await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); @@ -100,122 +66,12 @@ namespace Emby.Server.Implementations.Data innerProgress.RegisterAction(p => { double newPercentCommplete = 45 + .55 * p; - OnProgress(newPercentCommplete); progress.Report(newPercentCommplete); }); await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(100); await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); - - if (_config.Configuration.MigrationVersion < MigrationVersion) - { - _config.Configuration.MigrationVersion = MigrationVersion; - _config.SaveConfiguration(); - } - - if (_config.Configuration.SchemaVersion < LatestSchemaVersion) - { - _config.Configuration.SchemaVersion = LatestSchemaVersion; - _config.SaveConfiguration(); - } - - if (EnableUnavailableMessage) - { - EnableUnavailableMessage = false; - _httpServer.GlobalResponse = null; - _taskManager.QueueScheduledTask(); - } - - _taskManager.SuspendTriggers = false; - } - - private void OnProgress(double newPercentCommplete) - { - if (EnableUnavailableMessage) - { - var html = "Emby"; - var text = _localization.GetLocalizedString("DbUpgradeMessage"); - html += string.Format(text, newPercentCommplete.ToString("N2", CultureInfo.InvariantCulture)); - - html += ""; - html += ""; - - _httpServer.GlobalResponse = html; - } - } - - private async Task UpdateToLatestSchema(CancellationToken cancellationToken, IProgress progress) - { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery - { - IsCurrentSchema = false, - ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name } - }); - - var numComplete = 0; - var numItems = itemIds.Count; - - _logger.Debug("Upgrading schema for {0} items", numItems); - - var list = new List(); - - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (itemId != Guid.Empty) - { - // Somehow some invalid data got into the db. It probably predates the boundary checking - var item = _libraryManager.GetItemById(itemId); - - if (item != null) - { - list.Add(item); - } - } - - if (list.Count >= 1000) - { - try - { - await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } - - list.Clear(); - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); - } - - if (list.Count > 0) - { - try - { - await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } - } - - progress.Report(100); } private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 653a6a9c1..973576a0d 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -36,6 +36,7 @@ + @@ -64,6 +65,7 @@ + @@ -174,6 +176,7 @@ + @@ -258,6 +261,7 @@ + diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs new file mode 100644 index 000000000..424153f22 --- /dev/null +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -0,0 +1,59 @@ +using Emby.Server.Implementations.Browser; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Implementations.EntryPoints +{ + /// + /// Class StartupWizard + /// + public class StartupWizard : IServerEntryPoint + { + /// + /// The _app host + /// + private readonly IServerApplicationHost _appHost; + /// + /// The _user manager + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The app host. + /// The logger. + public StartupWizard(IServerApplicationHost appHost, ILogger logger) + { + _appHost = appHost; + _logger = logger; + } + + /// + /// Runs this instance. + /// + public void Run() + { + if (_appHost.IsFirstRun) + { + LaunchStartupWizard(); + } + } + + /// + /// Launches the startup wizard. + /// + private void LaunchStartupWizard() + { + BrowserLauncher.OpenDashboardPage("wizardstart.html", _appHost); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index 5be7ba7ad..48a85e0e0 100644 --- a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -74,7 +74,7 @@ namespace Emby.Server.Implementations.FileOrganization return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(15).Ticks} }; } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 876d140ec..c1758127a 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -59,12 +59,13 @@ namespace Emby.Server.Implementations.HttpServer private readonly IEnvironmentInfo _environment; private readonly IStreamFactory _streamFactory; private readonly Func> _funcParseFn; + private readonly bool _enableDualModeSockets; public HttpListenerHost(IServerApplicationHost applicationHost, ILogger logger, IServerConfigurationManager config, string serviceName, - string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn) + string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn, bool enableDualModeSockets) : base(serviceName) { _appHost = applicationHost; @@ -80,6 +81,7 @@ namespace Emby.Server.Implementations.HttpServer _certificate = certificate; _streamFactory = streamFactory; _funcParseFn = funcParseFn; + _enableDualModeSockets = enableDualModeSockets; _config = config; _logger = logger; @@ -179,8 +181,6 @@ namespace Emby.Server.Implementations.HttpServer private IHttpListener GetListener() { - var enableDualMode = _environment.OperatingSystem == OperatingSystem.Windows; - return new WebSocketSharpListener(_logger, _certificate, _memoryStreamProvider, @@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.HttpServer _socketFactory, _cryptoProvider, _streamFactory, - enableDualMode, + _enableDualModeSockets, GetRequest); } diff --git a/Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs b/Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs new file mode 100644 index 000000000..c532ea08d --- /dev/null +++ b/Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Updates; + +namespace Emby.Server.Implementations.Migrations +{ + public class UpdateLevelMigration : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private readonly IServerApplicationHost _appHost; + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + private readonly string _releaseAssetFilename; + private readonly ILogger _logger; + + public UpdateLevelMigration(IServerConfigurationManager config, IServerApplicationHost appHost, IHttpClient httpClient, IJsonSerializer jsonSerializer, string releaseAssetFilename, ILogger logger) + { + _config = config; + _appHost = appHost; + _httpClient = httpClient; + _jsonSerializer = jsonSerializer; + _releaseAssetFilename = releaseAssetFilename; + _logger = logger; + } + + public async Task Run() + { + var lastVersion = _config.Configuration.LastVersion; + var currentVersion = _appHost.ApplicationVersion; + + if (string.Equals(lastVersion, currentVersion.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return; + } + + try + { + var updateLevel = _config.Configuration.SystemUpdateLevel; + + await CheckVersion(currentVersion, updateLevel, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error in update migration", ex); + } + } + + private async Task CheckVersion(Version currentVersion, PackageVersionClass currentUpdateLevel, CancellationToken cancellationToken) + { + var releases = await new GithubUpdater(_httpClient, _jsonSerializer) + .GetLatestReleases("MediaBrowser", "Emby", _releaseAssetFilename, cancellationToken).ConfigureAwait(false); + + var newUpdateLevel = GetNewUpdateLevel(currentVersion, currentUpdateLevel, releases); + + if (newUpdateLevel != currentUpdateLevel) + { + _config.Configuration.SystemUpdateLevel = newUpdateLevel; + _config.SaveConfiguration(); + } + } + + private PackageVersionClass GetNewUpdateLevel(Version currentVersion, PackageVersionClass currentUpdateLevel, List releases) + { + var newUpdateLevel = currentUpdateLevel; + + // If the current version is later than current stable, set the update level to beta + if (releases.Count >= 1) + { + var release = releases[0]; + var version = ParseVersion(release.tag_name); + if (version != null) + { + if (currentVersion > version) + { + newUpdateLevel = PackageVersionClass.Beta; + } + else + { + return PackageVersionClass.Release; + } + } + } + + // If the current version is later than current beta, set the update level to dev + if (releases.Count >= 2) + { + var release = releases[1]; + var version = ParseVersion(release.tag_name); + if (version != null) + { + if (currentVersion > version) + { + newUpdateLevel = PackageVersionClass.Dev; + } + else + { + return PackageVersionClass.Beta; + } + } + } + + return newUpdateLevel; + } + + private Version ParseVersion(string versionString) + { + if (!string.IsNullOrWhiteSpace(versionString)) + { + var parts = versionString.Split('.'); + if (parts.Length == 3) + { + versionString += ".0"; + } + } + + Version version; + Version.TryParse(versionString, out version); + + return version; + } + } +} diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 5179bd258..f4cb42d29 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -40,7 +40,9 @@ namespace Emby.Server.Implementations.Security connection.RunInTransaction(db => { - AddColumn(db, "AccessTokens", "AppVersion", "TEXT"); + var existingColumnNames = GetColumnNames(db, "AccessTokens"); + + AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames); }); } } diff --git a/Emby.Server.Implementations/Sync/SyncRepository.cs b/Emby.Server.Implementations/Sync/SyncRepository.cs new file mode 100644 index 000000000..fbc5772f3 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncRepository.cs @@ -0,0 +1,770 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Sync; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncRepository : BaseSqliteRepository, ISyncRepository + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private readonly IJsonSerializer _json; + + public SyncRepository(ILogger logger, IJsonSerializer json, IServerApplicationPaths appPaths) + : base(logger) + { + _json = json; + DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db"); + } + + private class SyncSummary + { + public Dictionary Items { get; set; } + + public SyncSummary() + { + Items = new Dictionary(); + } + } + + public void Initialize() + { + using (var connection = CreateConnection()) + { + string[] queries = { + + "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", + + "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)", + + "drop index if exists idx_SyncJobItems2", + "drop index if exists idx_SyncJobItems3", + "drop index if exists idx_SyncJobs1", + "drop index if exists idx_SyncJobs", + "drop index if exists idx_SyncJobItems1", + "create index if not exists idx_SyncJobItems4 on SyncJobItems(TargetId,ItemId,Status,Progress,DateCreated)", + "create index if not exists idx_SyncJobItems5 on SyncJobItems(TargetId,Status,ItemId,Progress)", + + "create index if not exists idx_SyncJobs2 on SyncJobs(TargetId,Status,ItemIds,Progress)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries); + + connection.RunInTransaction(db => + { + var existingColumnNames = GetColumnNames(db, "SyncJobs"); + AddColumn(db, "SyncJobs", "Profile", "TEXT", existingColumnNames); + AddColumn(db, "SyncJobs", "Bitrate", "INT", existingColumnNames); + + existingColumnNames = GetColumnNames(db, "SyncJobItems"); + AddColumn(db, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT", existingColumnNames); + }); + } + } + + private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; + private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; + + public SyncJob GetJob(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + var guid = new Guid(id); + + if (guid == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = BaseJobSelectText + " where Id=?"; + var paramList = new List(); + + paramList.Add(guid.ToGuidParamValue()); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + return GetJob(row); + } + + return null; + } + } + } + + private SyncJob GetJob(IReadOnlyList reader) + { + var info = new SyncJob + { + Id = reader[0].ReadGuid().ToString("N"), + TargetId = reader[1].ToString(), + Name = reader[2].ToString() + }; + + if (reader[3].SQLiteType != SQLiteType.Null) + { + info.Profile = reader[3].ToString(); + } + + if (reader[4].SQLiteType != SQLiteType.Null) + { + info.Quality = reader[4].ToString(); + } + + if (reader[5].SQLiteType != SQLiteType.Null) + { + info.Bitrate = reader[5].ToInt(); + } + + if (reader[6].SQLiteType != SQLiteType.Null) + { + info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader[6].ToString(), true); + } + + if (reader[7].SQLiteType != SQLiteType.Null) + { + info.Progress = reader[7].ToDouble(); + } + + if (reader[8].SQLiteType != SQLiteType.Null) + { + info.UserId = reader[8].ToString(); + } + + if (reader[9].SQLiteType != SQLiteType.Null) + { + info.RequestedItemIds = reader[9].ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + + if (reader[10].SQLiteType != SQLiteType.Null) + { + info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader[10].ToString(), true); + } + + if (reader[11].SQLiteType != SQLiteType.Null) + { + info.ParentId = reader[11].ToString(); + } + + if (reader[12].SQLiteType != SQLiteType.Null) + { + info.UnwatchedOnly = reader[12].ToBool(); + } + + if (reader[13].SQLiteType != SQLiteType.Null) + { + info.ItemLimit = reader[13].ToInt(); + } + + info.SyncNewContent = reader[14].ToBool(); + + info.DateCreated = reader[15].ReadDateTime(); + info.DateLastModified = reader[16].ReadDateTime(); + info.ItemCount = reader[17].ToInt(); + + return info; + } + + public Task Create(SyncJob job) + { + return InsertOrUpdate(job, true); + } + + public Task Update(SyncJob job) + { + return InsertOrUpdate(job, false); + } + + private async Task InsertOrUpdate(SyncJob job, bool insert) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + string commandText; + var paramList = new List(); + + if (insert) + { + commandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + else + { + commandText = "update SyncJobs set TargetId=?,Name=?,Profile=?,Quality=?,Bitrate=?,Status=?,Progress=?,UserId=?,ItemIds=?,Category=?,ParentId=?,UnwatchedOnly=?,ItemLimit=?,SyncNewContent=?,DateCreated=?,DateLastModified=?,ItemCount=? where Id=?"; + } + + paramList.Add(job.Id.ToGuidParamValue()); + paramList.Add(job.TargetId); + paramList.Add(job.Name); + paramList.Add(job.Profile); + paramList.Add(job.Quality); + paramList.Add(job.Bitrate); + paramList.Add(job.Status.ToString()); + paramList.Add(job.Progress); + paramList.Add(job.UserId); + + paramList.Add(string.Join(",", job.RequestedItemIds.ToArray())); + paramList.Add(job.Category); + paramList.Add(job.ParentId); + paramList.Add(job.UnwatchedOnly); + paramList.Add(job.ItemLimit); + paramList.Add(job.SyncNewContent); + paramList.Add(job.DateCreated.ToDateTimeParamValue()); + paramList.Add(job.DateLastModified.ToDateTimeParamValue()); + paramList.Add(job.ItemCount); + + connection.RunInTransaction(conn => + { + conn.Execute(commandText, paramList.ToArray()); + }); + } + } + } + + public async Task DeleteJob(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(conn => + { + conn.Execute("delete from SyncJobs where Id=?", id.ToGuidParamValue()); + conn.Execute("delete from SyncJobItems where JobId=?", id); + }); + } + } + } + + public QueryResult GetJobs(SyncJobQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = BaseJobSelectText; + var paramList = new List(); + + var whereClauses = new List(); + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=?"); + paramList.Add(query.TargetId); + } + if (!string.IsNullOrWhiteSpace(query.ExcludeTargetIds)) + { + var excludeIds = (query.ExcludeTargetIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (excludeIds.Length == 1) + { + whereClauses.Add("TargetId<>?"); + paramList.Add(excludeIds[0]); + } + else if (excludeIds.Length > 1) + { + whereClauses.Add("TargetId<>?"); + paramList.Add(excludeIds[0]); + } + } + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + whereClauses.Add("UserId=?"); + paramList.Add(query.UserId); + } + if (query.SyncNewContent.HasValue) + { + whereClauses.Add("SyncNewContent=?"); + paramList.Add(query.SyncNewContent.Value); + } + + commandText += " mainTable"; + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + commandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; + + if (query.Limit.HasValue) + { + commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + var list = new List(); + var count = connection.Query("select count (Id) from SyncJobs" + whereTextWithoutPaging, paramList.ToArray()) + .SelectScalarInt() + .First(); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + list.Add(GetJob(row)); + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public SyncJobItem GetJobItem(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + lock (WriteLock) + { + var guid = new Guid(id); + + using (var connection = CreateConnection(true)) + { + var commandText = BaseJobItemSelectText + " where Id=?"; + var paramList = new List(); + + paramList.Add(guid.ToGuidParamValue()); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + return GetJobItem(row); + } + + return null; + } + } + } + + private QueryResult GetJobItemReader(SyncJobItemQuery query, string baseSelectText, Func, T> itemFactory) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = baseSelectText; + var paramList = new List(); + + var whereClauses = new List(); + + if (!string.IsNullOrWhiteSpace(query.JobId)) + { + whereClauses.Add("JobId=?"); + paramList.Add(query.JobId); + } + if (!string.IsNullOrWhiteSpace(query.ItemId)) + { + whereClauses.Add("ItemId=?"); + paramList.Add(query.ItemId); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=?"); + paramList.Add(query.TargetId); + } + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + commandText += " ORDER BY JobItemIndex, DateCreated"; + + if (query.Limit.HasValue) + { + commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + var list = new List(); + var count = connection.Query("select count (Id) from SyncJobItems" + whereTextWithoutPaging, paramList.ToArray()) + .SelectScalarInt() + .First(); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + list.Add(itemFactory(row)); + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) + { + var result = new Dictionary(); + + var now = DateTime.UtcNow; + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = "select ItemId,Status,Progress from SyncJobItems"; + + var whereClauses = new List(); + var paramList = new List(); + + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=?"); + paramList.Add(query.TargetId); + } + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + if (whereClauses.Count > 0) + { + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + AddStatusResult(row, result, false); + } + LogQueryTime("GetSyncedItemProgresses", commandText, now); + + commandText = commandText + .Replace("select ItemId,Status,Progress from SyncJobItems", "select ItemIds,Status,Progress from SyncJobs") + .Replace("'Synced'", "'Completed','CompletedWithError'"); + + now = DateTime.UtcNow; + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + AddStatusResult(row, result, true); + } + LogQueryTime("GetSyncedItemProgresses", commandText, now); + } + } + + return result; + } + + private void LogQueryTime(string methodName, string commandText, DateTime startDate) + { + var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; + + var slowThreshold = 1000; + +#if DEBUG + slowThreshold = 50; +#endif + + if (elapsed >= slowThreshold) + { + Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", + Convert.ToInt32(elapsed), + commandText, + methodName); + } + else + { + //Logger.Debug("{2} query time: {0}ms. Query: {1}", + // Convert.ToInt32(elapsed), + // cmd.CommandText, + // methodName); + } + } + + private void AddStatusResult(IReadOnlyList reader, Dictionary result, bool multipleIds) + { + if (reader[0].SQLiteType == SQLiteType.Null) + { + return; + } + + var itemIds = new List(); + + var ids = reader[0].ToString(); + + if (multipleIds) + { + itemIds = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + else + { + itemIds.Add(ids); + } + + if (reader[1].SQLiteType != SQLiteType.Null) + { + SyncJobItemStatus status; + var statusString = reader[1].ToString(); + if (string.Equals(statusString, "Completed", StringComparison.OrdinalIgnoreCase) || + string.Equals(statusString, "CompletedWithError", StringComparison.OrdinalIgnoreCase)) + { + status = SyncJobItemStatus.Synced; + } + else + { + status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), statusString, true); + } + + if (status == SyncJobItemStatus.Synced) + { + foreach (var itemId in itemIds) + { + result[itemId] = new SyncedItemProgress + { + Status = SyncJobItemStatus.Synced + }; + } + } + else + { + double progress = reader[2].SQLiteType == SQLiteType.Null ? 0.0 : reader[2].ToDouble(); + + foreach (var itemId in itemIds) + { + SyncedItemProgress currentStatus; + if (!result.TryGetValue(itemId, out currentStatus) || (currentStatus.Status != SyncJobItemStatus.Synced && progress >= currentStatus.Progress)) + { + result[itemId] = new SyncedItemProgress + { + Status = status, + Progress = progress + }; + } + } + } + } + } + + public QueryResult GetJobItems(SyncJobItemQuery query) + { + return GetJobItemReader(query, BaseJobItemSelectText, GetJobItem); + } + + public Task Create(SyncJobItem jobItem) + { + return InsertOrUpdate(jobItem, true); + } + + public Task Update(SyncJobItem jobItem) + { + return InsertOrUpdate(jobItem, false); + } + + private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert) + { + if (jobItem == null) + { + throw new ArgumentNullException("jobItem"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + string commandText; + + if (insert) + { + commandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + else + { + // cmd + commandText = "update SyncJobItems set ItemId=?,ItemName=?,MediaSourceId=?,JobId=?,TemporaryPath=?,OutputPath=?,Status=?,TargetId=?,DateCreated=?,Progress=?,AdditionalFiles=?,MediaSource=?,IsMarkedForRemoval=?,JobItemIndex=?,ItemDateModifiedTicks=? where Id=?"; + } + + var paramList = new List(); + paramList.Add(jobItem.Id.ToGuidParamValue()); + paramList.Add(jobItem.ItemId); + paramList.Add(jobItem.ItemName); + paramList.Add(jobItem.MediaSourceId); + paramList.Add(jobItem.JobId); + paramList.Add(jobItem.TemporaryPath); + paramList.Add(jobItem.OutputPath); + paramList.Add(jobItem.Status.ToString()); + + paramList.Add(jobItem.TargetId); + paramList.Add(jobItem.DateCreated.ToDateTimeParamValue()); + paramList.Add(jobItem.Progress); + paramList.Add(_json.SerializeToString(jobItem.AdditionalFiles)); + paramList.Add(jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource)); + paramList.Add(jobItem.IsMarkedForRemoval); + paramList.Add(jobItem.JobItemIndex); + paramList.Add(jobItem.ItemDateModifiedTicks); + + connection.RunInTransaction(conn => + { + conn.Execute(commandText, paramList.ToArray()); + }); + } + } + } + + private SyncJobItem GetJobItem(IReadOnlyList reader) + { + var info = new SyncJobItem + { + Id = reader[0].ReadGuid().ToString("N"), + ItemId = reader[1].ToString() + }; + + if (reader[2].SQLiteType != SQLiteType.Null) + { + info.ItemName = reader[2].ToString(); + } + + if (reader[3].SQLiteType != SQLiteType.Null) + { + info.MediaSourceId = reader[3].ToString(); + } + + info.JobId = reader[4].ToString(); + + if (reader[5].SQLiteType != SQLiteType.Null) + { + info.TemporaryPath = reader[5].ToString(); + } + if (reader[6].SQLiteType != SQLiteType.Null) + { + info.OutputPath = reader[6].ToString(); + } + + if (reader[7].SQLiteType != SQLiteType.Null) + { + info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader[7].ToString(), true); + } + + info.TargetId = reader[8].ToString(); + + info.DateCreated = reader[9].ReadDateTime(); + + if (reader[10].SQLiteType != SQLiteType.Null) + { + info.Progress = reader[10].ToDouble(); + } + + if (reader[11].SQLiteType != SQLiteType.Null) + { + var json = reader[11].ToString(); + + if (!string.IsNullOrWhiteSpace(json)) + { + info.AdditionalFiles = _json.DeserializeFromString>(json); + } + } + + if (reader[12].SQLiteType != SQLiteType.Null) + { + var json = reader[12].ToString(); + + if (!string.IsNullOrWhiteSpace(json)) + { + info.MediaSource = _json.DeserializeFromString(json); + } + } + + info.IsMarkedForRemoval = reader[13].ToBool(); + info.JobItemIndex = reader[14].ToInt(); + + if (reader[15].SQLiteType != SQLiteType.Null) + { + info.ItemDateModifiedTicks = reader[15].ToInt64(); + } + + return info; + } + } +} diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f3bab7883..a47aaa305 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -148,10 +148,6 @@ namespace Emby.Server.Implementations.TV private string GetUniqueSeriesKey(BaseItem series) { - if (_config.Configuration.SchemaVersion < 97) - { - return series.Id.ToString("N"); - } return series.GetPresentationUniqueKey(); } diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 49fdcece1..4e5047f78 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -115,7 +115,6 @@ namespace MediaBrowser.Api config.EnableStandaloneMusicKeys = true; config.EnableCaseSensitiveItemIds = true; config.EnableFolderView = true; - config.SchemaVersion = 109; config.EnableSimpleArtistDetection = true; config.SkipDeserializationForBasicTypes = true; config.SkipDeserializationForPrograms = true; diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 9e212219d..eb082f707 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -70,6 +70,7 @@ + diff --git a/MediaBrowser.Common/Updates/GithubUpdater.cs b/MediaBrowser.Common/Updates/GithubUpdater.cs new file mode 100644 index 000000000..c5000391d --- /dev/null +++ b/MediaBrowser.Common/Updates/GithubUpdater.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Common.Updates +{ + public class GithubUpdater + { + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + public GithubUpdater(IHttpClient httpClient, IJsonSerializer jsonSerializer) + { + _httpClient = httpClient; + _jsonSerializer = jsonSerializer; + } + + public async Task CheckForUpdateResult(string organzation, string repository, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename, TimeSpan cacheLength, CancellationToken cancellationToken) + { + var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); + + var options = new HttpRequestOptions + { + Url = url, + EnableKeepAlive = false, + CancellationToken = cancellationToken, + UserAgent = "Emby/3.0", + BufferContent = false + }; + + if (cacheLength.Ticks > 0) + { + options.CacheMode = CacheMode.Unconditional; + options.CacheLength = cacheLength; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + var obj = _jsonSerializer.DeserializeFromStream(stream); + + return CheckForUpdateResult(obj, minVersion, updateLevel, assetFilename, packageName, targetFilename); + } + } + + private CheckForUpdateResult CheckForUpdateResult(RootObject[] obj, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename) + { + if (updateLevel == PackageVersionClass.Release) + { + // Technically all we need to do is check that it's not pre-release + // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. + obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + else if (updateLevel == PackageVersionClass.Beta) + { + obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + else if (updateLevel == PackageVersionClass.Dev) + { + obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + var availableUpdate = obj + .Select(i => CheckForUpdateResult(i, minVersion, assetFilename, packageName, targetFilename)) + .Where(i => i != null) + .OrderByDescending(i => Version.Parse(i.AvailableVersion)) + .FirstOrDefault(); + + return availableUpdate ?? new CheckForUpdateResult + { + IsUpdateAvailable = false + }; + } + + private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel) + { + if (updateLevel == PackageVersionClass.Beta) + { + return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase); + } + if (updateLevel == PackageVersionClass.Dev) + { + return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || + i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); + } + + // Technically all we need to do is check that it's not pre-release + // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. + return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && + !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); + } + + public async Task> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken) + { + var list = new List(); + + var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); + + var options = new HttpRequestOptions + { + Url = url, + EnableKeepAlive = false, + CancellationToken = cancellationToken, + UserAgent = "Emby/3.0", + BufferContent = false + }; + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + var obj = _jsonSerializer.DeserializeFromStream(stream); + + obj = obj.Where(i => (i.assets ?? new List()).Any(a => IsAsset(a, assetFilename))).ToArray(); + + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1)); + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1)); + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1)); + + return list; + } + } + + public Version GetVersion(RootObject obj) + { + Version version; + if (!Version.TryParse(obj.tag_name, out version)) + { + return new Version(1, 0); + } + + return version; + } + + private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename) + { + Version version; + if (!Version.TryParse(obj.tag_name, out version)) + { + return null; + } + + if (version < minVersion) + { + return null; + } + + var asset = (obj.assets ?? new List()).FirstOrDefault(i => IsAsset(i, assetFilename)); + + if (asset == null) + { + return null; + } + + return new CheckForUpdateResult + { + AvailableVersion = version.ToString(), + IsUpdateAvailable = version > minVersion, + Package = new PackageVersionInfo + { + classification = obj.prerelease ? + (obj.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase) ? PackageVersionClass.Dev : PackageVersionClass.Beta) : + PackageVersionClass.Release, + name = packageName, + sourceUrl = asset.browser_download_url, + targetFilename = targetFilename, + versionStr = version.ToString(), + requiredVersionStr = "1.0.0", + description = obj.body, + infoUrl = obj.html_url + } + }; + } + + private bool IsAsset(Asset asset, string assetFilename) + { + var downloadFilename = Path.GetFileName(asset.browser_download_url) ?? string.Empty; + + if (downloadFilename.IndexOf(assetFilename, StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } + + return string.Equals(assetFilename, downloadFilename, StringComparison.OrdinalIgnoreCase); + } + + public class Uploader + { + public string login { get; set; } + public int id { get; set; } + public string avatar_url { get; set; } + public string gravatar_id { get; set; } + public string url { get; set; } + public string html_url { get; set; } + public string followers_url { get; set; } + public string following_url { get; set; } + public string gists_url { get; set; } + public string starred_url { get; set; } + public string subscriptions_url { get; set; } + public string organizations_url { get; set; } + public string repos_url { get; set; } + public string events_url { get; set; } + public string received_events_url { get; set; } + public string type { get; set; } + public bool site_admin { get; set; } + } + + public class Asset + { + public string url { get; set; } + public int id { get; set; } + public string name { get; set; } + public object label { get; set; } + public Uploader uploader { get; set; } + public string content_type { get; set; } + public string state { get; set; } + public int size { get; set; } + public int download_count { get; set; } + public string created_at { get; set; } + public string updated_at { get; set; } + public string browser_download_url { get; set; } + } + + public class Author + { + public string login { get; set; } + public int id { get; set; } + public string avatar_url { get; set; } + public string gravatar_id { get; set; } + public string url { get; set; } + public string html_url { get; set; } + public string followers_url { get; set; } + public string following_url { get; set; } + public string gists_url { get; set; } + public string starred_url { get; set; } + public string subscriptions_url { get; set; } + public string organizations_url { get; set; } + public string repos_url { get; set; } + public string events_url { get; set; } + public string received_events_url { get; set; } + public string type { get; set; } + public bool site_admin { get; set; } + } + + public class RootObject + { + public string url { get; set; } + public string assets_url { get; set; } + public string upload_url { get; set; } + public string html_url { get; set; } + public int id { get; set; } + public string tag_name { get; set; } + public string target_commitish { get; set; } + public string name { get; set; } + public bool draft { get; set; } + public Author author { get; set; } + public bool prerelease { get; set; } + public string created_at { get; set; } + public string published_at { get; set; } + public List assets { get; set; } + public string tarball_url { get; set; } + public string zipball_url { get; set; } + public string body { get; set; } + } + } +} diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 3fb118a9c..94baacf13 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -123,7 +123,6 @@ namespace MediaBrowser.Controller.Entities public int? MinParentalRating { get; set; } public int? MaxParentalRating { get; set; } - public bool? IsCurrentSchema { get; set; } public bool? HasDeadParentId { get; set; } public bool? IsOffline { get; set; } public bool? IsVirtualItem { get; set; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index cca8e3c19..a997d3476 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -126,10 +126,6 @@ namespace MediaBrowser.Controller.Entities.TV private static string GetUniqueSeriesKey(BaseItem series) { - if (ConfigurationManager.Configuration.SchemaVersion < 97) - { - return series.Id.ToString("N"); - } return series.GetPresentationUniqueKey(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 9715a624f..cdda858b7 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -192,9 +192,6 @@ namespace MediaBrowser.Model.Configuration public int SharingExpirationDays { get; set; } - public string[] Migrations { get; set; } - - public int MigrationVersion { get; set; } public int SchemaVersion { get; set; } public int SqliteCacheSize { get; set; } @@ -218,7 +215,6 @@ namespace MediaBrowser.Model.Configuration public ServerConfiguration() { LocalNetworkAddresses = new string[] { }; - Migrations = new string[] { }; CodecsUsed = new string[] { }; SqliteCacheSize = 0; ImageExtractionTimeoutMs = 0; diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index b6f847feb..fa3da97b3 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -74,7 +74,5 @@ namespace MediaBrowser.Model.Tasks event EventHandler> TaskExecuting; event EventHandler TaskCompleted; - - bool SuspendTriggers { get; set; } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/MonoAppHost.cs b/MediaBrowser.Server.Mono/MonoAppHost.cs index bb7db6a7c..d864c47d6 100644 --- a/MediaBrowser.Server.Mono/MonoAppHost.cs +++ b/MediaBrowser.Server.Mono/MonoAppHost.cs @@ -92,6 +92,32 @@ namespace MediaBrowser.Server.Mono MainClass.Shutdown(); } + protected override bool SupportsDualModeSockets + { + get + { + return GetMonoVersion() >= new Version(4, 6); + } + } + + private static Version GetMonoVersion() + { + Type type = Type.GetType("Mono.Runtime"); + if (type != null) + { + MethodInfo displayName = type.GetTypeInfo().GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + var displayNameValue = displayName.Invoke(null, null).ToString().Trim().Split(' ')[0]; + + Version version; + if (Version.TryParse(displayNameValue, out version)) + { + return version; + } + } + + return new Version(1, 0); + } + protected override void AuthorizeServer() { throw new NotImplementedException(); diff --git a/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs b/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs index 748b94604..3d6a3e35d 100644 --- a/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs +++ b/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs @@ -6,7 +6,8 @@ namespace MediaBrowser.Server.Mono.Native { public class MonoFileSystem : ManagedFileSystem { - public MonoFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars) : base(logger, supportsAsyncFileStreams, enableManagedInvalidFileNameChars, false) + public MonoFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars) + : base(logger, supportsAsyncFileStreams, enableManagedInvalidFileNameChars, true) { } diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index 9b634d12b..7eafc1721 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -23,8 +23,8 @@ using Emby.Common.Implementations.Logging; using Emby.Common.Implementations.Networking; using Emby.Common.Implementations.Security; using Emby.Server.Core; -using Emby.Server.Core.Browser; using Emby.Server.Implementations; +using Emby.Server.Implementations.Browser; using Emby.Server.Implementations.IO; using ImageMagickSharp; using MediaBrowser.Common.Net; diff --git a/MediaBrowser.ServerApplication/ServerNotifyIcon.cs b/MediaBrowser.ServerApplication/ServerNotifyIcon.cs index 139961e6f..c421dd9eb 100644 --- a/MediaBrowser.ServerApplication/ServerNotifyIcon.cs +++ b/MediaBrowser.ServerApplication/ServerNotifyIcon.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Logging; using System; using System.ComponentModel; using System.Windows.Forms; -using Emby.Server.Core.Browser; +using Emby.Server.Implementations.Browser; using MediaBrowser.Model.Globalization; namespace MediaBrowser.ServerApplication diff --git a/MediaBrowser.ServerApplication/WindowsAppHost.cs b/MediaBrowser.ServerApplication/WindowsAppHost.cs index b950de118..937762ed0 100644 --- a/MediaBrowser.ServerApplication/WindowsAppHost.cs +++ b/MediaBrowser.ServerApplication/WindowsAppHost.cs @@ -117,6 +117,14 @@ namespace MediaBrowser.ServerApplication } } + protected override bool SupportsDualModeSockets + { + get + { + return true; + } + } + public override void LaunchUrl(string url) { var process = new Process @@ -137,6 +145,7 @@ namespace MediaBrowser.ServerApplication } catch (Exception ex) { + Console.WriteLine("Error launching url: {0}", url); Logger.ErrorException("Error launching url: {0}", ex, url); throw; -- cgit v1.2.3 From 827602711e54bb966da0daa1e265e590a15a787d Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 20 Nov 2016 15:20:01 -0500 Subject: update sqlite dependencies --- Emby.Dlna/ContentDirectory/ControlHandler.cs | 2 +- .../Data/SqliteItemRepository.cs | 462 +++++++++++---------- .../FileOrganization/OrganizerScheduledTask.cs | 2 +- MediaBrowser.Controller/Entities/Person.cs | 4 +- .../People/TvdbPersonImageProvider.cs | 5 +- MediaBrowser.Server.Mac/Emby.Server.Mac.csproj | 6 - .../MediaBrowser.Server.Mono.csproj | 11 +- .../SQLitePCLRaw.provider.sqlite3.dll.config | 3 - MediaBrowser.Server.Mono/packages.config | 1 - MediaBrowser.XbmcMetadata/EntryPoint.cs | 2 +- 10 files changed, 259 insertions(+), 239 deletions(-) delete mode 100644 MediaBrowser.Server.Mono/SQLitePCLRaw.provider.sqlite3.dll.config (limited to 'Emby.Server.Implementations/FileOrganization') diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 8cb59b056..d77919e47 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -509,7 +509,7 @@ namespace Emby.Dlna.ContentDirectory { var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - Person = person.Name, + PersonIds = new[] { person.Id.ToString("N") }, IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name, typeof(Trailer).Name }, SortBy = new[] { ItemSortBy.SortName }, Limit = limit, diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index a3c1ee690..e4ceca267 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3434,8 +3434,23 @@ namespace Emby.Server.Implementations.Data if (query.PersonIds.Length > 0) { - // Todo: improve without having to do this - query.Person = query.PersonIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).FirstOrDefault(); + // TODO: Should this query with CleanName ? + + var clauses = new List(); + var index = 0; + foreach (var personId in query.PersonIds) + { + var paramName = "@PersonId" + index; + + clauses.Add("(select Name from TypedBaseItems where guid=" + paramName + ") in (select Name from People where ItemId=Guid)"); + if (statement != null) + { + statement.TryBind(paramName, personId.ToGuidParamValue()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); } if (!string.IsNullOrWhiteSpace(query.Person)) @@ -3633,25 +3648,36 @@ namespace Emby.Server.Implementations.Data var index = 0; foreach (var artistId in query.ExcludeArtistIds) { - var artistItem = RetrieveItem(new Guid(artistId)); - if (artistItem != null) + var paramName = "@ExcludeArtistId" + index; + + clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type<=1)"); + if (statement != null) { - clauses.Add("@ExcludeArtistName" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); - if (statement != null) - { - statement.TryBind("@ExcludeArtistName" + index, GetCleanValue(artistItem.Name)); - } - index++; + statement.TryBind(paramName, artistId.ToGuidParamValue()); } + index++; } - var clause = "(" + string.Join(" AND ", clauses.ToArray()) + ")"; + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; whereClauses.Add(clause); } if (query.GenreIds.Length > 0) { - // Todo: improve without having to do this - query.Genres = query.GenreIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + var clauses = new List(); + var index = 0; + foreach (var genreId in query.GenreIds) + { + var paramName = "@GenreId" + index; + + clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); + if (statement != null) + { + statement.TryBind(paramName, genreId.ToGuidParamValue()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); } if (query.Genres.Length > 0) @@ -3690,8 +3716,21 @@ namespace Emby.Server.Implementations.Data if (query.StudioIds.Length > 0) { - // Todo: improve without having to do this - query.Studios = query.StudioIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + var clauses = new List(); + var index = 0; + foreach (var studioId in query.StudioIds) + { + var paramName = "@StudioId" + index; + + clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=3)"); + if (statement != null) + { + statement.TryBind(paramName, studioId.ToGuidParamValue()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); } if (query.Studios.Length > 0) @@ -4655,205 +4694,204 @@ namespace Emby.Server.Implementations.Data ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); + InternalItemsQuery typeSubQuery = null; + + var itemCountColumns = new List>(); + + var typesToCount = query.IncludeItemTypes.ToList(); + + if (typesToCount.Count > 0) + { + var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); + + typeSubQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + var whereClauses = GetWhereClauses(typeSubQuery, null, "itemTypes"); + + whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); + + var typeWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + itemCountColumnQuery += typeWhereText; + + //itemCountColumnQuery += ")"; + + itemCountColumns.Add(new Tuple("itemTypes", "(" + itemCountColumnQuery + ") as itemTypes")); + } + + var columns = _retriveItemColumns.ToList(); + columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); + + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, columns.ToArray())) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var innerQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + + var innerWhereClauses = GetWhereClauses(innerQuery, null); + + var innerWhereText = innerWhereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", innerWhereClauses.ToArray()); + + var whereText = " where Type=@SelectType"; + + if (typesToCount.Count == 0) + { + whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + else + { + //whereText += " And itemTypes not null"; + whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + + var outerQuery = new InternalItemsQuery(query.User) + { + IsFavorite = query.IsFavorite, + IsFavoriteOrLiked = query.IsFavoriteOrLiked, + IsLiked = query.IsLiked, + IsLocked = query.IsLocked, + NameLessThan = query.NameLessThan, + NameStartsWith = query.NameStartsWith, + NameStartsWithOrGreater = query.NameStartsWithOrGreater, + AlbumArtistStartsWithOrGreater = query.AlbumArtistStartsWithOrGreater, + Tags = query.Tags, + OfficialRatings = query.OfficialRatings, + GenreIds = query.GenreIds, + Genres = query.Genres, + Years = query.Years + }; + + var outerWhereClauses = GetWhereClauses(outerQuery, null); + + whereText += outerWhereClauses.Count == 0 ? + string.Empty : + " AND " + string.Join(" AND ", outerWhereClauses.ToArray()); + //cmd.CommandText += GetGroupBy(query); + + commandText += whereText; + commandText += " group by PresentationUniqueKey"; + + commandText += " order by SortName"; + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; + + var list = new List>(); + var count = 0; + using (WriteLock.Write()) { - // var itemCountColumns = new List>(); - - // var typesToCount = query.IncludeItemTypes.ToList(); - - // if (typesToCount.Count > 0) - // { - // var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); - - // var typeSubQuery = new InternalItemsQuery(query.User) - // { - // ExcludeItemTypes = query.ExcludeItemTypes, - // IncludeItemTypes = query.IncludeItemTypes, - // MediaTypes = query.MediaTypes, - // AncestorIds = query.AncestorIds, - // ExcludeItemIds = query.ExcludeItemIds, - // ItemIds = query.ItemIds, - // TopParentIds = query.TopParentIds, - // ParentId = query.ParentId, - // IsPlayed = query.IsPlayed - // }; - // var whereClauses = GetWhereClauses(typeSubQuery, cmd, "itemTypes"); - - // whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); - - // var typeWhereText = whereClauses.Count == 0 ? - // string.Empty : - // " where " + string.Join(" AND ", whereClauses.ToArray()); - - // itemCountColumnQuery += typeWhereText; - - // //itemCountColumnQuery += ")"; - - // itemCountColumns.Add(new Tuple("itemTypes", "(" + itemCountColumnQuery + ") as itemTypes")); - // } - - // var columns = _retriveItemColumns.ToList(); - // columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); - - // cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, columns.ToArray(), cmd)) + GetFromText(); - // cmd.CommandText += GetJoinUserDataText(query); - - // var innerQuery = new InternalItemsQuery(query.User) - // { - // ExcludeItemTypes = query.ExcludeItemTypes, - // IncludeItemTypes = query.IncludeItemTypes, - // MediaTypes = query.MediaTypes, - // AncestorIds = query.AncestorIds, - // ExcludeItemIds = query.ExcludeItemIds, - // ItemIds = query.ItemIds, - // TopParentIds = query.TopParentIds, - // ParentId = query.ParentId, - // IsPlayed = query.IsPlayed - // }; - - // var innerWhereClauses = GetWhereClauses(innerQuery, cmd); - - // var innerWhereText = innerWhereClauses.Count == 0 ? - // string.Empty : - // " where " + string.Join(" AND ", innerWhereClauses.ToArray()); - - // var whereText = " where Type=@SelectType"; - - // if (typesToCount.Count == 0) - // { - // whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; - // } - // else - // { - // //whereText += " And itemTypes not null"; - // whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; - // } - - // var outerQuery = new InternalItemsQuery(query.User) - // { - // IsFavorite = query.IsFavorite, - // IsFavoriteOrLiked = query.IsFavoriteOrLiked, - // IsLiked = query.IsLiked, - // IsLocked = query.IsLocked, - // NameLessThan = query.NameLessThan, - // NameStartsWith = query.NameStartsWith, - // NameStartsWithOrGreater = query.NameStartsWithOrGreater, - // AlbumArtistStartsWithOrGreater = query.AlbumArtistStartsWithOrGreater, - // Tags = query.Tags, - // OfficialRatings = query.OfficialRatings, - // GenreIds = query.GenreIds, - // Genres = query.Genres, - // Years = query.Years - // }; - - // var outerWhereClauses = GetWhereClauses(outerQuery, cmd); - - // whereText += outerWhereClauses.Count == 0 ? - // string.Empty : - // " AND " + string.Join(" AND ", outerWhereClauses.ToArray()); - // //cmd.CommandText += GetGroupBy(query); - - // cmd.CommandText += whereText; - // cmd.CommandText += " group by PresentationUniqueKey"; - - // cmd.Parameters.Add(cmd, "@SelectType", DbType.String).Value = returnType; - - // if (EnableJoinUserData(query)) - // { - // cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; - // } - - // cmd.CommandText += " order by SortName"; - - // if (query.Limit.HasValue || query.StartIndex.HasValue) - // { - // var offset = query.StartIndex ?? 0; - - // if (query.Limit.HasValue || offset > 0) - // { - // cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - // } - - // if (offset > 0) - // { - // cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - // } - // } - - // cmd.CommandText += ";"; - - // var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - // if (isReturningZeroItems) - // { - // cmd.CommandText = ""; - // } - - // if (query.EnableTotalRecordCount) - // { - // cmd.CommandText += "select count (distinct PresentationUniqueKey)" + GetFromText(); - - // cmd.CommandText += GetJoinUserDataText(query); - // cmd.CommandText += whereText; - // } - // else - // { - // cmd.CommandText = cmd.CommandText.TrimEnd(';'); - // } - - // var list = new List>(); - // var count = 0; - - // var commandBehavior = isReturningZeroItems || !query.EnableTotalRecordCount - // ? (CommandBehavior.SequentialAccess | CommandBehavior.SingleResult) - // : CommandBehavior.SequentialAccess; - - // //Logger.Debug("GetItemValues: " + cmd.CommandText); - - // using (var reader = cmd.ExecuteReader(commandBehavior)) - // { - // LogQueryTime("GetItemValues", cmd, now); - - // if (isReturningZeroItems) - // { - // if (reader.Read()) - // { - // count = reader.GetInt32(0); - // } - // } - // else - // { - // while (reader.Read()) - // { - // var item = GetItem(reader); - // if (item != null) - // { - // var countStartColumn = columns.Count - 1; - - // list.Add(new Tuple(item, GetItemCounts(reader, countStartColumn, typesToCount))); - // } - // } - - // if (reader.NextResult() && reader.Read()) - // { - // count = reader.GetInt32(0); - // } - // } - // } - - // if (count == 0) - // { - // count = list.Count; - // } - - // return new QueryResult> - // { - // Items = list.ToArray(), - // TotalRecordCount = count - // }; - } - - return new QueryResult>(); + if (!isReturningZeroItems) + { + using (var statement = _connection.PrepareStatement(commandText)) + { + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null, "itemTypes"); + } + BindSimilarParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row); + if (item != null) + { + var countStartColumn = columns.Count - 1; + + list.Add(new Tuple(item, GetItemCounts(row, countStartColumn, typesToCount))); + } + } + + LogQueryTime("GetItemValues", commandText, now); + } + } + + if (query.EnableTotalRecordCount) + { + commandText = "select count (distinct PresentationUniqueKey)" + GetFromText(); + + commandText += GetJoinUserDataText(query); + commandText += whereText; + + using (var statement = _connection.PrepareStatement(commandText)) + { + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null, "itemTypes"); + } + BindSimilarParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + count = statement.ExecuteQuery().SelectScalarInt().First(); + + LogQueryTime("GetItemValues", commandText, now); + } + } + } + + if (count == 0) + { + count = list.Count; + } + + return new QueryResult> + { + Items = list.ToArray(), + TotalRecordCount = count + }; } private ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, List typesToCount) diff --git a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index 48a85e0e0..5be7ba7ad 100644 --- a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -74,7 +74,7 @@ namespace Emby.Server.Implementations.FileOrganization return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(15).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} }; } diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 79d2a2ee5..0c36442af 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Entities public IEnumerable GetTaggedItems(InternalItemsQuery query) { - query.Person = Name; + query.PersonIds = new[] { Id.ToString("N") }; return LibraryManager.GetItemList(query); } @@ -95,7 +95,7 @@ namespace MediaBrowser.Controller.Entities { var itemsWithPerson = LibraryManager.GetItemIds(new InternalItemsQuery { - Person = Name + PersonIds = new[] { Id.ToString("N") } }); return inputItems.Where(i => itemsWithPerson.Contains(i.Id)); diff --git a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs index 817b9d71a..0384ddd25 100644 --- a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs @@ -62,13 +62,10 @@ namespace MediaBrowser.Providers.People public Task> GetImages(IHasImages item, CancellationToken cancellationToken) { - // Avoid implicitly captured closure - var itemName = item.Name; - var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Series).Name }, - Person = itemName + PersonIds = new[] { item.Id.ToString("N") } }).Cast() .Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds)) diff --git a/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj b/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj index f80513c51..9df2df2ff 100644 --- a/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj +++ b/MediaBrowser.Server.Mac/Emby.Server.Mac.csproj @@ -115,9 +115,6 @@ ..\packages\SQLitePCLRaw.provider.sqlite3.net45.1.1.1-pre20161109081005\lib\net45\SQLitePCLRaw.provider.sqlite3.dll True - - ..\ThirdParty\System.Data.SQLite.ManagedOnly\1.0.94.0\System.Data.SQLite.dll - ..\ThirdParty\emby\Emby.Common.Implementations.dll @@ -151,9 +148,6 @@ - - Native\SqliteExtensions.cs - diff --git a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj index 13d67ace5..485bbf3a9 100644 --- a/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj +++ b/MediaBrowser.Server.Mono/MediaBrowser.Server.Mono.csproj @@ -88,16 +88,15 @@ ..\packages\SQLitePCLRaw.core.1.1.1-pre20161109081005\lib\net45\SQLitePCLRaw.core.dll True - - ..\packages\SQLitePCLRaw.provider.sqlite3.net45.1.1.1-pre20161109081005\lib\net45\SQLitePCLRaw.provider.sqlite3.dll - True - ..\ThirdParty\MediaBrowser.IsoMounting.Linux\MediaBrowser.IsoMounting.Linux.dll + + ..\ThirdParty\SQLitePCLRaw.provider.sqlite3.net45\System.Data.SQLite.dll + @@ -223,10 +222,6 @@ PreserveNewest - - Designer - PreserveNewest - PreserveNewest Designer diff --git a/MediaBrowser.Server.Mono/SQLitePCLRaw.provider.sqlite3.dll.config b/MediaBrowser.Server.Mono/SQLitePCLRaw.provider.sqlite3.dll.config deleted file mode 100644 index 83a6cd9f3..000000000 --- a/MediaBrowser.Server.Mono/SQLitePCLRaw.provider.sqlite3.dll.config +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/packages.config b/MediaBrowser.Server.Mono/packages.config index e82ef0ed0..6e3fe428c 100644 --- a/MediaBrowser.Server.Mono/packages.config +++ b/MediaBrowser.Server.Mono/packages.config @@ -6,5 +6,4 @@ - \ No newline at end of file diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs index bf3d3c303..cd18e5670 100644 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -52,7 +52,7 @@ namespace MediaBrowser.XbmcMetadata var items = _libraryManager.GetItemList(new InternalItemsQuery { - Person = person.Name + PersonIds = new [] { person.Id.ToString("N") } }).ToList(); -- cgit v1.2.3 From e1b880a5a072764cabace79cd6d1d65315ec65e4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 13 Dec 2016 02:36:30 -0500 Subject: update series queries --- .../Activity/ActivityRepository.cs | 15 +- .../Data/BaseSqliteRepository.cs | 2 + .../Data/SqliteItemRepository.cs | 261 +++++++++++---------- .../Data/SqliteUserDataRepository.cs | 12 +- Emby.Server.Implementations/Dto/DtoService.cs | 105 ++++++--- .../FileOrganization/EpisodeFileOrganizer.cs | 6 + .../SocketSharp/WebSocketSharpListener.cs | 3 +- .../Library/LibraryManager.cs | 58 +++-- .../Library/UserDataManager.cs | 7 +- .../Notifications/SqliteNotificationsRepository.cs | 20 +- .../Security/AuthenticationRepository.cs | 12 +- MediaBrowser.Api/BaseApiService.cs | 32 ++- MediaBrowser.Api/ItemLookupService.cs | 15 +- MediaBrowser.Controller/Entities/BaseItem.cs | 3 +- .../Entities/CollectionFolder.cs | 68 ++++-- MediaBrowser.Controller/Entities/Folder.cs | 36 +-- MediaBrowser.Controller/Entities/IHasUserData.cs | 6 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 2 + .../Library/IUserDataManager.cs | 6 +- MediaBrowser.Model/Querying/ItemFields.cs | 2 + RSSDP/SsdpDevicePublisherBase.cs | 8 +- 21 files changed, 418 insertions(+), 261 deletions(-) (limited to 'Emby.Server.Implementations/FileOrganization') diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index 7ac0e680c..5f6518a14 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -84,9 +84,6 @@ namespace Emby.Server.Implementations.Activity { using (var connection = CreateConnection(true)) { - var list = new List(); - var result = new QueryResult(); - var commandText = BaseActivitySelectText; var whereClauses = new List(); @@ -127,8 +124,11 @@ namespace Emby.Server.Implementations.Activity statementTexts.Add(commandText); statementTexts.Add("select count (Id) from ActivityLogEntries" + whereTextWithoutPaging); - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + var result = new QueryResult(); + var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())).ToList(); using (var statement = statements[0]) @@ -153,10 +153,11 @@ namespace Emby.Server.Implementations.Activity result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } - }, ReadTransactionMode); - result.Items = list.ToArray(); - return result; + result.Items = list.ToArray(); + return result; + + }, ReadTransactionMode); } } } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 9e60a43aa..2fc721f83 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -43,6 +43,7 @@ namespace Emby.Server.Implementations.Data //CheckOk(rc); rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1); + //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1); //CheckOk(rc); rc = raw.sqlite3_enable_shared_cache(1); @@ -94,6 +95,7 @@ namespace Emby.Server.Implementations.Data var queries = new List { //"PRAGMA cache size=-10000" + //"PRAGMA read_uncommitted = true" }; if (EnableTempStoreMemory) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 42cbf1965..803ebeca0 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -328,6 +328,8 @@ namespace Emby.Server.Implementations.Data "drop table if exists Images", "drop index if exists idx_Images", "drop index if exists idx_TypeSeriesPresentationUniqueKey", + "drop index if exists idx_SeriesPresentationUniqueKey", + "drop index if exists idx_TypeSeriesPresentationUniqueKey2", "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", @@ -343,8 +345,9 @@ namespace Emby.Server.Implementations.Data // series "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - // series next up - "create index if not exists idx_SeriesPresentationUniqueKey on TypedBaseItems(SeriesPresentationUniqueKey)", + // series counts + // seriesdateplayed sort order + "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", // live tv programs "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", @@ -2079,25 +2082,29 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("id"); } - var list = new List(); - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) + return connection.RunInTransaction(db => { - statement.TryBind("@ItemId", id); + var list = new List(); - foreach (var row in statement.ExecuteQuery()) + using (var statement = PrepareStatementSafe(db, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { - list.Add(GetChapter(row)); + statement.TryBind("@ItemId", id); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetChapter(row)); + } } - } + + return list; + + }, ReadTransactionMode); } } - - return list; } /// @@ -2470,32 +2477,33 @@ namespace Emby.Server.Implementations.Data //commandText += GetGroupBy(query); - int count = 0; - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, commandText)) + return connection.RunInTransaction(db => { - if (EnableJoinUserData(query)) + using (var statement = PrepareStatementSafe(db, commandText)) { - statement.TryBind("@UserId", query.User.Id); - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - BindSimilarParams(query, statement); + BindSimilarParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - count = statement.ExecuteQuery().SelectScalarInt().First(); - } + var count = statement.ExecuteQuery().SelectScalarInt().First(); + LogQueryTime("GetCount", commandText, now); + return count; + } + + }, ReadTransactionMode); } - LogQueryTime("GetCount", commandText, now); } - - return count; } public List GetItemList(InternalItemsQuery query) @@ -2511,8 +2519,6 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var list = new List(); - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2553,53 +2559,59 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, commandText)) + return connection.RunInTransaction(db => { - if (EnableJoinUserData(query)) + var list = new List(); + + using (var statement = PrepareStatementSafe(db, commandText)) { - statement.TryBind("@UserId", query.User.Id); - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - BindSimilarParams(query, statement); + BindSimilarParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query); - if (item != null) + foreach (var row in statement.ExecuteQuery()) { - list.Add(item); + var item = GetItem(row, query); + if (item != null) + { + list.Add(item); + } } } - } - } - LogQueryTime("GetItemList", commandText, now); - } + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.EnableGroupByMetadataKey) + { + var limit = query.Limit ?? int.MaxValue; + limit -= 4; + var newList = new List(); - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.EnableGroupByMetadataKey) - { - var limit = query.Limit ?? int.MaxValue; - limit -= 4; - var newList = new List(); + foreach (var item in list) + { + AddItem(newList, item); - foreach (var item in list) - { - AddItem(newList, item); + if (newList.Count >= limit) + { + break; + } + } - if (newList.Count >= limit) - { - break; - } - } + list = newList; + } - list = newList; - } + LogQueryTime("GetItemList", commandText, now); - return list; + return list; + + }, ReadTransactionMode); + } + } } private void AddItem(List items, BaseItem newItem) @@ -2637,7 +2649,7 @@ namespace Emby.Server.Implementations.Data var slowThreshold = 1000; #if DEBUG - slowThreshold = 50; + slowThreshold = 2; #endif if (elapsed >= slowThreshold) @@ -2718,7 +2730,6 @@ namespace Emby.Server.Implementations.Data } } - var result = new QueryResult(); var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; var statementTexts = new List(); @@ -2748,8 +2759,9 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var result = new QueryResult(); var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())) .ToList(); @@ -2796,12 +2808,12 @@ namespace Emby.Server.Implementations.Data } } - }, ReadTransactionMode); + LogQueryTime("GetItems", commandText, now); - LogQueryTime("GetItems", commandText, now); + result.Items = list.ToArray(); + return result; - result.Items = list.ToArray(); - return result; + }, ReadTransactionMode); } } } @@ -2962,34 +2974,38 @@ namespace Emby.Server.Implementations.Data } } - var list = new List(); - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, commandText)) + return connection.RunInTransaction(db => { - if (EnableJoinUserData(query)) + var list = new List(); + + using (var statement = PrepareStatementSafe(db, commandText)) { - statement.TryBind("@UserId", query.User.Id); - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - BindSimilarParams(query, statement); + BindSimilarParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row[0].ReadGuid()); + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row[0].ReadGuid()); + } } - } - } - LogQueryTime("GetItemList", commandText, now); + LogQueryTime("GetItemList", commandText, now); - return list; + return list; + + }, ReadTransactionMode); + } } } @@ -3153,10 +3169,10 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - var result = new QueryResult(); - - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var result = new QueryResult(); + var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())) .ToList(); @@ -3199,12 +3215,12 @@ namespace Emby.Server.Implementations.Data } } - }, ReadTransactionMode); + LogQueryTime("GetItemIds", commandText, now); - LogQueryTime("GetItemIds", commandText, now); + result.Items = list.ToArray(); + return result; - result.Items = list.ToArray(); - return result; + }, ReadTransactionMode); } } } @@ -4653,13 +4669,13 @@ namespace Emby.Server.Implementations.Data commandText += " order by ListOrder"; - var list = new List(); using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); using (var statement = PrepareStatementSafe(db, commandText)) { // Run this again to bind the params @@ -4670,9 +4686,9 @@ namespace Emby.Server.Implementations.Data list.Add(row.GetString(0)); } } + return list; }, ReadTransactionMode); } - return list; } } @@ -4696,14 +4712,14 @@ namespace Emby.Server.Implementations.Data commandText += " order by ListOrder"; - var list = new List(); - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + using (var statement = PrepareStatementSafe(db, commandText)) { // Run this again to bind the params @@ -4714,11 +4730,11 @@ namespace Emby.Server.Implementations.Data list.Add(GetPerson(row)); } } + + return list; }, ReadTransactionMode); } } - - return list; } private List GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) @@ -4899,8 +4915,6 @@ namespace Emby.Server.Implementations.Data ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); - var list = new List(); - var commandText = "Select Value From ItemValues where " + typeClause; if (withItemTypes.Count > 0) @@ -4920,8 +4934,10 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + using (var statement = PrepareStatementSafe(db, commandText)) { foreach (var row in statement.ExecuteQuery()) @@ -4932,12 +4948,13 @@ namespace Emby.Server.Implementations.Data } } } + + LogQueryTime("GetItemValueNames", commandText, now); + + return list; }, ReadTransactionMode); } } - LogQueryTime("GetItemValueNames", commandText, now); - - return list; } private QueryResult> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) @@ -5081,9 +5098,6 @@ namespace Emby.Server.Implementations.Data var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var list = new List>(); - var result = new QueryResult>(); - var statementTexts = new List(); if (!isReturningZeroItems) { @@ -5102,8 +5116,11 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List>(); + var result = new QueryResult>(); + var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())).ToList(); if (!isReturningZeroItems) @@ -5167,17 +5184,18 @@ namespace Emby.Server.Implementations.Data LogQueryTime("GetItemValues", commandText, now); } } + + if (result.TotalRecordCount == 0) + { + result.TotalRecordCount = list.Count; + } + result.Items = list.ToArray(); + + return result; + }, ReadTransactionMode); } } - - if (result.TotalRecordCount == 0) - { - result.TotalRecordCount = list.Count; - } - result.Items = list.ToArray(); - - return result; } private ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, List typesToCount) @@ -5390,8 +5408,6 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("query"); } - var list = new List(); - var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; cmdText += " ItemId=@ItemId"; @@ -5412,8 +5428,10 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + using (var statement = PrepareStatementSafe(db, cmdText)) { statement.TryBind("@ItemId", query.ItemId.ToGuidParamValue()); @@ -5433,11 +5451,12 @@ namespace Emby.Server.Implementations.Data list.Add(GetMediaStream(row)); } } + + return list; + }, ReadTransactionMode); } } - - return list; } public async Task SaveMediaStreams(Guid id, List streams, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 7afb5720e..7767ae892 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -300,9 +300,7 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - UserItemData result = null; - - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { using (var statement = db.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key =@Key and userId=@UserId")) { @@ -311,13 +309,13 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { - result = ReadRow(row); - break; + return ReadRow(row); } } - }, ReadTransactionMode); - return result; + return null; + + }, ReadTransactionMode); } } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2b2c3e000..d0c473777 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -459,12 +459,21 @@ namespace Emby.Server.Implementations.Dto if (dtoOptions.EnableUserData) { - dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false); + dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user, dtoOptions.Fields).ConfigureAwait(false); } if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library) { - dto.ChildCount = GetChildCount(folder, user); + // For these types we can try to optimize and assume these values will be equal + if (item is MusicAlbum || item is Season) + { + dto.ChildCount = dto.RecursiveItemCount; + } + + if (dtoOptions.Fields.Contains(ItemFields.ChildCount)) + { + dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user); + } } if (fields.Contains(ItemFields.CumulativeRunTimeTicks)) @@ -1151,28 +1160,29 @@ namespace Emby.Server.Implementations.Dto { dto.Artists = hasArtist.Artists; - var artistItems = _libraryManager.GetArtists(new InternalItemsQuery - { - EnableTotalRecordCount = false, - ItemIds = new[] { item.Id.ToString("N") } - }); - - dto.ArtistItems = artistItems.Items - .Select(i => - { - var artist = i.Item1; - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - }) - .ToList(); + //var artistItems = _libraryManager.GetArtists(new InternalItemsQuery + //{ + // EnableTotalRecordCount = false, + // ItemIds = new[] { item.Id.ToString("N") } + //}); + + //dto.ArtistItems = artistItems.Items + // .Select(i => + // { + // var artist = i.Item1; + // return new NameIdPair + // { + // Name = artist.Name, + // Id = artist.Id.ToString("N") + // }; + // }) + // .ToList(); // Include artists that are not in the database yet, e.g., just added via metadata editor - var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); + //var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); + dto.ArtistItems = new List(); dto.ArtistItems.AddRange(hasArtist.Artists - .Except(foundArtists, new DistinctNameComparer()) + //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it @@ -1201,23 +1211,48 @@ namespace Emby.Server.Implementations.Dto { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); - var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery - { - EnableTotalRecordCount = false, - ItemIds = new[] { item.Id.ToString("N") } - }); - - dto.AlbumArtists = artistItems.Items + //var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery + //{ + // EnableTotalRecordCount = false, + // ItemIds = new[] { item.Id.ToString("N") } + //}); + + //dto.AlbumArtists = artistItems.Items + // .Select(i => + // { + // var artist = i.Item1; + // return new NameIdPair + // { + // Name = artist.Name, + // Id = artist.Id.ToString("N") + // }; + // }) + // .ToList(); + + dto.AlbumArtists = new List(); + dto.AlbumArtists.AddRange(hasAlbumArtist.AlbumArtists + //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { - var artist = i.Item1; - return new NameIdPair + // This should not be necessary but we're seeing some cases of it + if (string.IsNullOrWhiteSpace(i)) { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - }) - .ToList(); + return null; + } + + var artist = _libraryManager.GetArtist(i); + if (artist != null) + { + return new NameIdPair + { + Name = artist.Name, + Id = artist.Id.ToString("N") + }; + } + + return null; + + }).Where(i => i != null)); } // Add video info diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index cc2dcb6fd..5bb21d02a 100644 --- a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -519,6 +519,12 @@ namespace Emby.Server.Implementations.FileOrganization private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) { + // We should probably handle this earlier so that we never even make it this far + if (string.Equals(result.OriginalPath, result.TargetPath, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index 94cc383a7..4606d0e31 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -76,7 +76,8 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp private void ProcessContext(HttpListenerContext context) { - Task.Factory.StartNew(() => InitTask(context)); + //Task.Factory.StartNew(() => InitTask(context), TaskCreationOptions.DenyChildAttach | TaskCreationOptions.PreferFairness); + Task.Run(() => InitTask(context)); } private Task InitTask(HttpListenerContext context) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ad91988e5..1ff61286f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -817,7 +817,31 @@ namespace Emby.Server.Implementations.Library return _userRootFolder; } - + + public Guid? FindIdByPath(string path, bool? isFolder) + { + // If this returns multiple items it could be tricky figuring out which one is correct. + // In most cases, the newest one will be and the others obsolete but not yet cleaned up + + var query = new InternalItemsQuery + { + Path = path, + IsFolder = isFolder, + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + Limit = 1 + }; + + var id = GetItemIds(query); + + if (id.Count == 0) + { + return null; + } + + return id[0]; + } + public BaseItem FindByPath(string path, bool? isFolder) { // If this returns multiple items it could be tricky figuring out which one is correct. @@ -1430,7 +1454,7 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); query.AncestorIds = new string[] { }; } } @@ -1489,7 +1513,7 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); } else { @@ -1515,11 +1539,11 @@ namespace Emby.Server.Implementations.Library }, CancellationToken.None).Result.ToList(); - query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).Select(i => i.ToString("N")).ToArray(); } } - private IEnumerable GetTopParentsForQuery(BaseItem item, User user) + private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user) { var view = item as UserView; @@ -1527,7 +1551,7 @@ namespace Emby.Server.Implementations.Library { if (string.Equals(view.ViewType, CollectionType.LiveTv)) { - return new[] { view }; + return new[] { view.Id }; } if (string.Equals(view.ViewType, CollectionType.Channels)) { @@ -1537,7 +1561,7 @@ namespace Emby.Server.Implementations.Library }, CancellationToken.None).Result; - return channelResult.Items; + return channelResult.Items.Select(i => i.Id); } // Translate view into folders @@ -1546,18 +1570,18 @@ namespace Emby.Server.Implementations.Library var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) { - return GetTopParentsForQuery(displayParent, user); + return GetTopParentIdsForQuery(displayParent, user); } - return new BaseItem[] { }; + return new Guid[] { }; } if (view.ParentId != Guid.Empty) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) { - return GetTopParentsForQuery(displayParent, user); + return GetTopParentIdsForQuery(displayParent, user); } - return new BaseItem[] { }; + return new Guid[] { }; } // Handle grouping @@ -1568,23 +1592,23 @@ namespace Emby.Server.Implementations.Library .OfType() .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) .Where(i => user.IsFolderGrouped(i.Id)) - .SelectMany(i => GetTopParentsForQuery(i, user)); + .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return new BaseItem[] { }; + return new Guid[] { }; } var collectionFolder = item as CollectionFolder; if (collectionFolder != null) { - return collectionFolder.GetPhysicalParents(); + return collectionFolder.PhysicalFolderIds; } - + var topParent = item.GetTopParent(); if (topParent != null) { - return new[] { topParent }; + return new[] { topParent.Id }; } - return new BaseItem[] { }; + return new Guid[] { }; } /// diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index f4a30fc00..5a14edf13 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -12,6 +12,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Library { @@ -186,16 +187,16 @@ namespace Emby.Server.Implementations.Library var userData = GetUserData(user.Id, item); var dto = GetUserItemDataDto(userData); - await item.FillUserDataDtoValues(dto, userData, null, user).ConfigureAwait(false); + await item.FillUserDataDtoValues(dto, userData, null, user, new List()).ConfigureAwait(false); return dto; } - public async Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user) + public async Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, List fields) { var userData = GetUserData(user.Id, item); var dto = GetUserItemDataDto(userData); - await item.FillUserDataDtoValues(dto, userData, itemDto, user).ConfigureAwait(false); + await item.FillUserDataDtoValues(dto, userData, itemDto, user, fields).ConfigureAwait(false); return dto; } diff --git a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs index 43e19da65..f18278cb2 100644 --- a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs +++ b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs @@ -65,9 +65,9 @@ namespace Emby.Server.Implementations.Notifications var whereClause = " where " + string.Join(" And ", clauses.ToArray()); - using (var connection = CreateConnection(true)) + using (WriteLock.Read()) { - lock (WriteLock) + using (var connection = CreateConnection(true)) { result.TotalRecordCount = connection.Query("select count(Id) from Notifications" + whereClause, paramList.ToArray()).SelectScalarInt().First(); @@ -106,9 +106,9 @@ namespace Emby.Server.Implementations.Notifications { var result = new NotificationsSummary(); - using (var connection = CreateConnection(true)) + using (WriteLock.Read()) { - lock (WriteLock) + using (var connection = CreateConnection(true)) { using (var statement = connection.PrepareStatement("select Level from Notifications where UserId=@UserId and IsRead=@IsRead")) { @@ -223,9 +223,9 @@ namespace Emby.Server.Implementations.Notifications cancellationToken.ThrowIfCancellationRequested(); - using (var connection = CreateConnection()) + lock (WriteLock) { - lock (WriteLock) + using (var connection = CreateConnection()) { connection.RunInTransaction(conn => { @@ -286,9 +286,9 @@ namespace Emby.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - using (var connection = CreateConnection()) + using (WriteLock.Write()) { - lock (WriteLock) + using (var connection = CreateConnection()) { connection.RunInTransaction(conn => { @@ -308,9 +308,9 @@ namespace Emby.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - using (var connection = CreateConnection()) + using (WriteLock.Write()) { - lock (WriteLock) + using (var connection = CreateConnection()) { connection.RunInTransaction(conn => { diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 392db6935..7199f4f4d 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -206,10 +206,10 @@ namespace Emby.Server.Implementations.Security { using (var connection = CreateConnection(true)) { - var result = new QueryResult(); - - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var result = new QueryResult(); + var statementTexts = new List(); statementTexts.Add(commandText); statementTexts.Add("select count (Id) from AccessTokens" + whereTextWithoutPaging); @@ -236,10 +236,10 @@ namespace Emby.Server.Implementations.Security } } - }, ReadTransactionMode); + result.Items = list.ToArray(); + return result; - result.Items = list.ToArray(); - return result; + }, ReadTransactionMode); } } } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 73a2bedb9..7a8951396 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -121,7 +121,9 @@ namespace MediaBrowser.Api { var options = new DtoOptions(); - options.DeviceId = authContext.GetAuthorizationInfo(Request).DeviceId; + var authInfo = authContext.GetAuthorizationInfo(Request); + + options.DeviceId = authInfo.DeviceId; var hasFields = request as IHasItemFields; if (hasFields != null) @@ -129,6 +131,34 @@ namespace MediaBrowser.Api options.Fields = hasFields.GetItemFields().ToList(); } + var client = authInfo.Client ?? string.Empty; + if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) + { + options.Fields.Add(Model.Querying.ItemFields.RecursiveItemCount); + } + + if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1) + { + options.Fields.Add(Model.Querying.ItemFields.ChildCount); + } + + if (client.IndexOf("web", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("mobile", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("ios", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("android", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("theater", StringComparison.OrdinalIgnoreCase) == -1) + { + options.Fields.Add(Model.Querying.ItemFields.ChildCount); + } + var hasDtoOptions = request as IHasDtoOptions; if (hasDtoOptions != null) { diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index 0ae1fbff4..b5c51bfef 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -14,8 +14,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; @@ -88,6 +86,12 @@ namespace MediaBrowser.Api { } + [Route("/Items/RemoteSearch/Book", "POST")] + [Authenticated] + public class GetBookRemoteSearchResults : RemoteSearchQuery, IReturn> + { + } + [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")] public class GetRemoteSearchImage { @@ -147,6 +151,13 @@ namespace MediaBrowser.Api return ToOptimizedResult(result); } + public async Task Post(GetBookRemoteSearchResults request) + { + var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + public async Task Post(GetMovieRemoteSearchResults request) { var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f4503466..2aa53d651 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -30,6 +30,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities @@ -2191,7 +2192,7 @@ namespace MediaBrowser.Controller.Entities return path; } - public virtual Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user) + public virtual Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, List itemFields) { if (RunTimeTicks.HasValue) { diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index ebc55ca8a..c505aefb3 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -27,6 +27,7 @@ namespace MediaBrowser.Controller.Entities public CollectionFolder() { PhysicalLocationsList = new List(); + PhysicalFolderIds = new List(); } [IgnoreDataMember] @@ -153,6 +154,7 @@ namespace MediaBrowser.Controller.Entities } public List PhysicalLocationsList { get; set; } + public List PhysicalFolderIds { get; set; } protected override IEnumerable GetFileSystemChildren(IDirectoryService directoryService) { @@ -176,6 +178,18 @@ namespace MediaBrowser.Controller.Entities } } + if (!changed) + { + var folderIds = PhysicalFolderIds.ToList(); + + var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + } + } + return changed; } @@ -186,6 +200,31 @@ namespace MediaBrowser.Controller.Entities return changed; } + protected override bool RefreshLinkedChildren(IEnumerable fileSystemChildren) + { + var physicalFolders = GetPhysicalFolders(false) + .ToList(); + + var linkedChildren = physicalFolders + .SelectMany(c => c.LinkedChildren) + .ToList(); + + var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer()); + + LinkedChildren = linkedChildren; + + var folderIds = PhysicalFolderIds.ToList(); + var newFolderIds = physicalFolders.Select(i => i.Id).ToList(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + PhysicalFolderIds = newFolderIds.ToList(); + } + + return changed; + } + internal override bool IsValidFromResolver(BaseItem newItem) { var newCollectionFolder = newItem as CollectionFolder; @@ -260,26 +299,6 @@ namespace MediaBrowser.Controller.Entities return Task.FromResult(true); } - /// - /// Our children are actually just references to the ones in the physical root... - /// - /// The linked children. - [IgnoreDataMember] - public override List LinkedChildren - { - get { return GetLinkedChildrenInternal(); } - set - { - base.LinkedChildren = value; - } - } - private List GetLinkedChildrenInternal() - { - return GetPhysicalParents() - .SelectMany(c => c.LinkedChildren) - .ToList(); - } - /// /// Our children are actually just references to the ones in the physical root... /// @@ -292,11 +311,16 @@ namespace MediaBrowser.Controller.Entities private IEnumerable GetActualChildren() { - return GetPhysicalParents().SelectMany(c => c.Children); + return GetPhysicalFolders(true).SelectMany(c => c.Children); } - public IEnumerable GetPhysicalParents() + private IEnumerable GetPhysicalFolders(bool enableCache) { + if (enableCache) + { + return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType(); + } + var rootChildren = LibraryManager.RootFolder.Children .OfType() .ToList(); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 4705f03fa..a84e9a5d2 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1222,7 +1222,7 @@ namespace MediaBrowser.Controller.Entities /// Refreshes the linked children. /// /// true if XXXX, false otherwise - private bool RefreshLinkedChildren(IEnumerable fileSystemChildren) + protected virtual bool RefreshLinkedChildren(IEnumerable fileSystemChildren) { var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList(); var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); @@ -1410,23 +1410,24 @@ namespace MediaBrowser.Controller.Entities } } - public override async Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user) + public override async Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, List itemFields) { if (!SupportsUserDataFromChildren) { return; } - var recursiveItemCount = GetRecursiveChildCount(user); - if (itemDto != null) { - itemDto.RecursiveItemCount = recursiveItemCount; + if (itemFields.Contains(ItemFields.RecursiveItemCount)) + { + itemDto.RecursiveItemCount = GetRecursiveChildCount(user); + } } - if (recursiveItemCount > 0 && SupportsPlayedStatus) + if (SupportsPlayedStatus) { - var unplayedQueryResult = recursiveItemCount > 0 ? await GetItems(new InternalItemsQuery(user) + var unplayedQueryResult = await GetItems(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, @@ -1435,21 +1436,24 @@ namespace MediaBrowser.Controller.Entities Limit = 0, IsPlayed = false - }).ConfigureAwait(false) : new QueryResult(); + }).ConfigureAwait(false); double unplayedCount = unplayedQueryResult.TotalRecordCount; - var unplayedPercentage = (unplayedCount / recursiveItemCount) * 100; - dto.PlayedPercentage = 100 - unplayedPercentage; - dto.Played = dto.PlayedPercentage.Value >= 100; dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; - } - if (itemDto != null) - { - if (this is Season || this is MusicAlbum) + if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + { + if (itemDto.RecursiveItemCount.Value > 0) + { + var unplayedPercentage = (unplayedCount/itemDto.RecursiveItemCount.Value)*100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; + } + } + else { - itemDto.ChildCount = recursiveItemCount; + dto.Played = (dto.UnplayedItemCount ?? 0) == 0; } } } diff --git a/MediaBrowser.Controller/Entities/IHasUserData.cs b/MediaBrowser.Controller/Entities/IHasUserData.cs index c21e170ae..0b3b7dc8d 100644 --- a/MediaBrowser.Controller/Entities/IHasUserData.cs +++ b/MediaBrowser.Controller/Entities/IHasUserData.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities { @@ -14,10 +15,7 @@ namespace MediaBrowser.Controller.Entities /// /// Fills the user data dto values. /// - /// The dto. - /// The user data. - /// The user. - Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user); + Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, List fields); bool EnableRememberingTrackSelections { get; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index d297fd006..bf9a07626 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -62,6 +62,8 @@ namespace MediaBrowser.Controller.Library /// BaseItem. BaseItem FindByPath(string path, bool? isFolder); + Guid? FindIdByPath(string path, bool? isFolder); + /// /// Gets the artist. /// diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index 86c52c4c3..5940c7e29 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -5,6 +5,7 @@ using MediaBrowser.Model.Entities; using System; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Library { @@ -37,12 +38,9 @@ namespace MediaBrowser.Controller.Library /// /// Gets the user data dto. /// - /// The item. - /// The user. - /// UserItemDataDto. Task GetUserDataDto(IHasUserData item, User user); - Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user); + Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, List fields); /// /// Get all user data for the given user diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index bf1d4991c..e36abf16e 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -171,6 +171,8 @@ /// PrimaryImageAspectRatio, + RecursiveItemCount, + /// /// The revenue /// diff --git a/RSSDP/SsdpDevicePublisherBase.cs b/RSSDP/SsdpDevicePublisherBase.cs index 260c00896..8ab35d661 100644 --- a/RSSDP/SsdpDevicePublisherBase.cs +++ b/RSSDP/SsdpDevicePublisherBase.cs @@ -291,7 +291,7 @@ namespace Rssdp.Infrastructure if (devices != null) { var deviceList = devices.ToList(); - WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); + //WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); foreach (var device in deviceList) { @@ -300,7 +300,7 @@ namespace Rssdp.Infrastructure } else { - WriteTrace(String.Format("Sending 0 search responses.")); + //WriteTrace(String.Format("Sending 0 search responses.")); } }); } @@ -413,7 +413,7 @@ namespace Rssdp.Infrastructure //DisposeRebroadcastTimer(); - WriteTrace("Begin Sending Alive Notifications For All Devices"); + //WriteTrace("Begin Sending Alive Notifications For All Devices"); _LastNotificationTime = DateTime.Now; @@ -430,7 +430,7 @@ namespace Rssdp.Infrastructure SendAliveNotifications(device, true); } - WriteTrace("Completed Sending Alive Notifications For All Devices"); + //WriteTrace("Completed Sending Alive Notifications For All Devices"); } catch (ObjectDisposedException ex) { -- cgit v1.2.3