///
/// Copyright © 2003-2008 JetBrains s.r.o.
/// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder).
///
using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using JetBrains.Omea.Base;
using JetBrains.Omea.Diagnostics;
using JetBrains.Omea.OpenAPI;
using JetBrains.Omea.ResourceTools;
namespace JetBrains.Omea.RSSPlugin
{
///
/// If a feed item ("RSSItem" resource type) has an enclosure attached (has property ), indicates the downloading state.
/// Defines possible values for the property stored in .
///
public enum EnclosureDownloadState : int
{
///
/// Minimum value (inclusive).
///
MinValue = 0,
///
/// The enclosure is available, but has not been downloaded yet, and the downloading has not been planned.
///
NotDownloaded = 0,
///
/// Schedulled for downloading.
///
Planned = 1,
///
/// The downloading has completed successfully.
///
Completed = 2,
///
/// Failed downloading an enclosure, enclosure not available locally.
///
Failed = 3,
///
/// The enclosure is currently being downloaded.
///
InProgress = 4,
///
/// The limiting value (non-inclusive).
///
MaxValue = 5
}
///
/// Wraps into a class.
/// (H) Dunno why, maybe for late binding?..
///
public abstract class DownloadState
{
public const int NotDownloaded = (int)EnclosureDownloadState.NotDownloaded;
public const int Planned = (int)EnclosureDownloadState.Planned;
public const int Completed = (int)EnclosureDownloadState.Completed;
public const int Failed = (int)EnclosureDownloadState.Failed;
public const int InProgress = (int)EnclosureDownloadState.InProgress;
}
///
/// Implements the enclosures support.
///
public class EnclosureDownloadManager
{
public static DateTime GetStartDownloadDateTime()
{
DateTime result = DateTime.Now;
DateTimeFormatInfo info = CultureInfo.CurrentCulture.DateTimeFormat;
if ( Settings.UseEclosureDownloadPeriod )
{
try
{
string startStr = Settings.EnclosureDownloadStartHour;
DateTime start = new DateTime( DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 0, 0, 0 );
DateTime startTime = DateTime.Parse( startStr, info );
start = start.AddHours( startTime.Hour ).AddMinutes( startTime.Minute );
string finishStr = Settings.EnclosureDownloadFinishHour;
DateTime finish = new DateTime( DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 0, 0, 0 );
DateTime finishTime = DateTime.Parse( finishStr, info );
finish = finish.AddHours( finishTime.Hour ).AddMinutes( finishTime.Minute );
if ( start >= finish )
{
finish = finish.AddDays( 1 );
}
if ( DateTime.Now < start )
{
result = start;
}
if ( DateTime.Now > finish )
{
result = start.AddDays( 1 );
}
}
catch( Exception )
{
result = DateTime.Now;
}
}
return result;
}
public static void PlanToDownload( IResource feedItem )
{
PlanToDownload( feedItem, null );
}
public static void PlanToDownload( IResource feedItem, string folder )
{
if ( feedItem.GetPropText( Props.EnclosureURL ).Trim().Length > 0 )
{
ResourceProxy proxy = new ResourceProxy( feedItem );
proxy.BeginUpdate();
proxy.SetProp( Props.EnclosureDownloadingState, DownloadState.Planned );
proxy.DeleteProp( Props.EnclosureFailureReason );
proxy.DeleteProp( Props.EnclosureTempFile );
proxy.DeleteProp( Props.EnclosureDownloadedSize );
if( !string.IsNullOrEmpty( folder ) )
proxy.SetProp( Props.EnclosurePath, folder );
else
proxy.DeleteProp( Props.EnclosurePath );
proxy.EndUpdate();
DownloadNextEnclosure();
}
}
public static void CancelDownload( IResource feedItem )
{
new ResourceProxy( feedItem ).SetProp( Props.EnclosureDownloadingState, DownloadState.NotDownloaded );
}
private static void QueueToDownload( IResource feedItem )
{
if ( DownloadEnclosure.Queued )
{
if ( Core.ResourceStore.FindResources( "RSSItem", Props.EnclosureDownloadingState, DownloadState.InProgress ).Count == 0 )
{
Core.ResourceAP.CancelJobs( new JobFilter( DownloadEnclosure.CancelJob ) );
Core.ResourceAP.CancelTimedJobs( new JobFilter( DownloadEnclosure.CancelJob ) );
DownloadEnclosure.Queued = false;
}
}
if ( !DownloadEnclosure.Queued )
{
DownloadEnclosure.Do( GetStartDownloadDateTime(), feedItem );
}
}
public static void DownloadNextEnclosure()
{
IResourceList list =
Core.ResourceStore.FindResources( "RSSItem", Props.EnclosureDownloadingState, DownloadState.InProgress );
if ( list.Count > 0 )
{
QueueToDownload( list[0] );
return;
}
list =
Core.ResourceStore.FindResources( "RSSItem", Props.EnclosureDownloadingState, DownloadState.Planned );
if ( list.Count > 0 )
{
QueueToDownload( list[0] );
}
}
#region Enclosure State Icons
///
/// Returns a 16 by 16 icon that indicates a particular state of downloading the enclosure.
///
/// Enclosure download state.
/// The icon (the same instance for the same parameters).
public static Icon GetEnclosureStateIcon( EnclosureDownloadState state )
{
if( !Core.UserInterfaceAP.IsOwnerThread )
throw new InvalidOperationException( "This method must be accessed only from the User Interface Async Processor thread." );
// Load the icons on the first call
if( _arEnclosureStateIcons == null )
{
_arEnclosureStateIcons = new Icon[5];
String[] sNames = new[] {"NotDownloaded", "Planned", "Completed", "Failed", "InProgress"};
for( int a = 0; a < sNames.Length; a++ )
_arEnclosureStateIcons[ a ] = RSSPlugin.LoadIconFromAssembly( string.Format( "download{0}.ico", sNames[ a ] ) );
}
// Range check
if( (state < EnclosureDownloadState.MinValue) || (state >= EnclosureDownloadState.MaxValue) )
throw new ArgumentException( "The enclosure download state is out of range." );
// Hand out the icon
return _arEnclosureStateIcons[ (int)state ];
}
///
/// Caches the enclosure state icons to avoid loading them more than once
///
protected static Icon[] _arEnclosureStateIcons = null;
#endregion
}
internal class DownloadEnclosure : DownloadFileJob
{
private readonly IResource _resource;
private static bool _queued = false;
private readonly string _directory;
private readonly int _startPosition = 0;
private static readonly char[] addedIllChars = {'\\', '/', ':', '?', '\"' };
private DownloadEnclosure( IResource resource, FileStream file, string directory, int startPosition )
: base( resource.GetStringProp( Props.EnclosureURL ), file, startPosition )
{
Guard.NullArgument(resource, "resource");
Guard.EmptyStringArgument(directory, "directory");
_resource = resource;
_directory = ValidatePath(directory);
_startPosition = startPosition;
}
private static string ValidatePath(string path)
{
Guard.EmptyStringArgument( path, "path" );
foreach ( char invalid in Path.InvalidPathChars )
path = path.Replace( invalid, ' ' );
return path;
}
//---------------------------------------------------------------------
// Path.InvalidPathChars used in the ValidatePath:
// 1. Obsolete in .Net 2/3
// 2. Does not contain several obviously errorneous symbols which
// prevent creating directory (at least on XP)
// This method perfroms simple and compatible workaround.
//---------------------------------------------------------------------
private static string ValidateName( string name )
{
name = ValidatePath( name );
foreach ( char invalid in addedIllChars )
name = name.Replace( invalid, ' ' );
return name;
}
public static bool CancelJob( AbstractJob job ) // returns true - cancel, returns false - do not cancel
{
return job is DownloadEnclosure;
}
public static void Do( DateTime at, IResource feedItem )
{
string enclosureUrl = feedItem.GetPropText( Props.EnclosureURL ).Trim();
if ( enclosureUrl.Length == 0 )
{
return;
}
IResource feed = feedItem.GetLinkProp( -Props.RSSItem );
string directory = FindDownloadDirectory( feedItem, feed );
try
{
Directory.CreateDirectory( directory );
string destFullPath = null;
FileStream file = null;
int startPosition = 0;
if ( feedItem.GetIntProp( Props.EnclosureDownloadingState ) == DownloadState.InProgress )
{
string enclosureTempFile = feedItem.GetPropText( Props.EnclosureTempFile );
if ( File.Exists( enclosureTempFile ) )
{
try
{
file = File.OpenWrite( enclosureTempFile );
destFullPath = enclosureTempFile;
startPosition = (int)file.Length;
file.Seek( startPosition, SeekOrigin.Begin );
}
catch ( Exception exception )
{
Tracer._TraceException( exception );
}
}
}
if ( destFullPath == null && file == null )
{
destFullPath = FindFreeFileName( enclosureUrl, directory, true );
file = File.Create( destFullPath );
}
new ResourceProxy( feedItem ).SetProp( Props.EnclosureTempFile, destFullPath );
Core.NetworkAP.QueueJobAt( at, new DownloadEnclosure( feedItem, file, directory, startPosition ) );
_queued = true;
}
catch ( Exception exception )
{
_queued = false;
ResourceProxy proxy = new ResourceProxy( feedItem );
proxy.BeginUpdate();
proxy.SetProp( Props.EnclosureDownloadingState, DownloadState.Failed );
proxy.SetProp( Props.EnclosureFailureReason, exception.Message );
proxy.EndUpdate();
ShowDesktopAlert( DownloadState.Failed, "Downloading Failed", feedItem.DisplayName, exception.Message );
EnclosureDownloadManager.DownloadNextEnclosure();
}
}
//---------------------------------------------------------------------
// First look at the feed item - if it is downloaded through the rule
// action then the path is set. Otherwise, the specific path can be
// set for a particular feed. Finally take the default path from settings
// and (if necessary) append the name of the feed.
//---------------------------------------------------------------------
private static string FindDownloadDirectory( IResource feedItem, IResource feed )
{
string dir = null;
if( feedItem.HasProp( Props.EnclosurePath ) )
{
dir = feedItem.GetStringProp( Props.EnclosurePath );
}
else
{
dir = feed.GetStringProp( Props.EnclosurePath );
if ( string.IsNullOrEmpty( dir ) )
{
dir = Settings.EnclosurePath;
string feedName = feed.GetPropText( Core.Props.Name );
if ( Settings.CreateSubfolderForEveryFeed && !string.IsNullOrEmpty( feedName ))
{
dir = Path.Combine( dir, ValidateName( feedName ) );
}
}
dir = ValidatePath( dir );
}
return dir;
}
private static void ShowDesktopAlert( int downloadState, string from, string subject, string body )
{
if ( !Settings.ShowDesktopAlertWhenEncosureDownloadingComplete && downloadState == DownloadState.Completed )
{
return;
}
if ( !Settings.ShowDesktopAlertWhenEncosureDownloadingFailed && downloadState == DownloadState.Failed )
{
return;
}
Core.UIManager.ShowDesktopAlert( EnclosureDownloadStateColumn.Instance.ImageList, downloadState, from,
subject, body, null );
}
private static string FindFreeFileName( string enclosureUrl, string directory, bool isPart )
{
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension( enclosureUrl );
string extension = Path.GetExtension( enclosureUrl );
if ( isPart )
extension += ".part";
fileNameWithoutExtension = ValidateName( fileNameWithoutExtension );
extension = ValidateName( extension );
string destFullPath = Path.Combine( directory, fileNameWithoutExtension );
destFullPath += extension;
int postFix = 0;
while ( File.Exists( destFullPath ) )
{
++postFix;
destFullPath = Path.Combine( directory, fileNameWithoutExtension + postFix + extension );
}
destFullPath = ValidatePath( destFullPath );
return destFullPath;
}
protected override void Execute()
{
if ( _resource.Id != -1 && _resource.GetIntProp( Props.EnclosureDownloadingState ) != DownloadState.NotDownloaded )
{
// Determine if we want update some of the resource properties (create proxy for that)
int nTotalLength = GetLength();
bool bUpdateState = _resource.GetIntProp( Props.EnclosureDownloadingState ) != DownloadState.InProgress;
bool bUpdateLength = ((nTotalLength > 0) && (nTotalLength != _resource.GetIntProp( Props.EnclosureSize )));
if( (bUpdateState) || (bUpdateLength) )
{
ResourceProxy proxy = new ResourceProxy( _resource );
proxy.BeginUpdate();
if( bUpdateState )
proxy.SetProp( Props.EnclosureDownloadingState, DownloadState.InProgress );
if( bUpdateLength )
proxy.SetProp( Props.EnclosureSize, nTotalLength + _startPosition );
proxy.EndUpdateAsync();
}
// Check if the downloaded size should be updated
// As we take the actual size from the disc file, not from the property, its accuracy is not needed; overwrite the property only if the percent value changes, as it's used only for the percentage display
if( nTotalLength > 0 ) // The total-size info is available (if it's not, we do not have the percentage anyway)
{
int nSize = GetDownloadedSize(); // Size we've downloaded
int nOldSize = _resource.GetIntProp( Props.EnclosureDownloadedSize ); // The prev size property value
if( (nSize * 100 / nTotalLength) != (nOldSize * 100 / nTotalLength) ) // Percentage has changed, should write a new value
Core.UserInterfaceAP.QueueJob( "Update Enclosure Downloaded Size", new UpdateDownloadedSizeDelegate( UpdateDownloadedSize ), nSize ); // Schedulle to the UI AP so that it merges the too-frequent updates for enclosures that download too fast, that reduces flicker
}
// Do the download step
base.Execute();
}
else
{
InvokeAfterWait( null, null );
new ResourceProxy( _resource ).SetProp( Props.EnclosureDownloadingState, DownloadState.NotDownloaded );
_queued = false;
EnclosureDownloadManager.DownloadNextEnclosure();
}
}
public static bool Queued { get { return _queued; } set { _queued = value; } }
protected override void Ready( )
{
ResourceProxy proxy = new ResourceProxy( _resource );
proxy.BeginUpdate();
if ( !Successfull )
{
proxy.SetProp( Props.EnclosureDownloadingState, DownloadState.Failed );
string lastException = null;
if ( LastException != null )
{
lastException = LastException.Message;
}
proxy.SetProp( Props.EnclosureFailureReason, lastException );
proxy.DeleteProp( Props.EnclosureTempFile );
ShowDesktopAlert( DownloadState.Failed, "Downloading Failed", _resource.DisplayName, lastException );
}
else
{
proxy.SetProp( Props.EnclosureDownloadingState, DownloadState.Completed );
ShowDesktopAlert( DownloadState.Completed, "Downloading Completed", _resource.DisplayName, null );
}
proxy.EndUpdate();
_queued = false;
EnclosureDownloadManager.DownloadNextEnclosure();
}
protected override void StoreStream( Stream stream )
{
string path = string.Empty;
FileStream file = FileStream;
file.Close();
try
{
path = FindFreeFileName( Url, _directory, false );
File.Move( file.Name, path );
new ResourceProxy( _resource ).SetProp( Props.EnclosureTempFile, path );
}
catch ( IOException exception )
{
Tracer._TraceException( exception );
}
catch ( ArgumentException exception )
{
Tracer._TraceException( exception );
Core.UIManager.ShowSimpleMessageBox( "Download Enclosure Failed", "Illegal characters in path: \'" + path + "\'" );
}
}
///
/// Updates the downloaded size of an enclosure.
///
protected void UpdateDownloadedSize(int nNewSize)
{
if( _resource.IsDeleted )
return;
new ResourceProxy( _resource ).SetPropAsync( Props.EnclosureDownloadedSize, nNewSize );
}
///
/// Delegate for .
///
public delegate void UpdateDownloadedSizeDelegate(int nNewSize);
}
}