PSDropNew/DropboxProvider.cs
using System;
using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Provider; using System.Management.Automation.Runspaces; using System.Net; using System.Reflection; using System.Security; using System.Text.RegularExpressions; using System.Threading; using DropNet; using DropNet.Exceptions; using DropNet.Models; using IntelliTect.Management.Automation; using IntelliTect.Security; using Newtonsoft.Json; namespace IntelliTect.PSDropbin { [CmdletProvider( ProviderName, ProviderCapabilities.Credentials | ProviderCapabilities.ExpandWildcards )] public class DropboxProvider : NavigationCmdletProvider { public DropboxProvider() { ProviderEventArgs<DropboxProvider>.PublishNewProviderInstance( this, new ProviderEventArgs<DropboxProvider>( this ) ); } #region Data private const string ProviderName = "Dropbox"; private DropNetClient Client => ( (DropboxDriveInfo) PSDriveInfo ).Client; #endregion #region Drive Management protected override bool IsValidPath( string path ) { return !string.IsNullOrEmpty( path ) && Path.GetInvalidPathChars().All( c => !path.Contains( c ) ); } protected override PSDriveInfo RemoveDrive(PSDriveInfo drive) { if (drive == null) { throw new ArgumentNullException(nameof(drive)); } return base.RemoveDrive(drive); } protected override PSDriveInfo NewDrive( PSDriveInfo drive) { if ( drive == null ) { throw new ArgumentNullException( nameof( drive ) ); } string credentialName = DropboxDriveInfo.GetDropboxCredentialName(drive.Name); // If the credential doesn't already exist, prompt for it. if (CredentialManager.ReadCredential(credentialName) == null) { WriteWarning($"Couldn't find Dropbox Credentials for drive {drive.Name}."); if (!PromptForCredential(drive)) { WriteWarning("Couldn't get Dropbox Credentials. Run New-PSDrive again when ready."); return null; } } return new DropboxDriveInfo(drive); } private bool PromptForCredential(PSDriveInfo driveInfo) { var client = new DropNetClient( Settings.Default.ApiKey, Settings.Default.AppSecret); var token = client.GetToken(); var url = client.BuildAuthorizeUrl(); WriteObject("Opening URL: " + url); // open browser for authentication try { Process.Start(url); } catch (Exception) { WriteWarning("An unexpected error occured while opening the browser."); } WriteObject("Waiting for authentication..."); // poll for authentication until it either occurs or you give up in frustration int counter = 15; while (counter > 0) { try { // if we make it through this segment, a token is successfully generated and saved var accessToken = client.GetAccessToken(); CredentialManager.WriteCredential( DropboxDriveInfo.GetDropboxCredentialName(driveInfo.Name), accessToken.Token, accessToken.Secret); break; } catch (Exception) { Thread.Sleep(5000); counter--; } } if (counter <= 0) { WriteWarning("Authentication failed."); return false; } else { WriteObject("Authentication successful. Run Remove-DropboxCredential to remove stored credentials."); return true; } } protected override Collection<PSDriveInfo> InitializeDefaultDrives() { Collection<PSDriveInfo> drives = base.InitializeDefaultDrives(); // We used to always initialized a single, default drive here, and that's all this supported. // Now, we require the user to call New-PSDrive (alias mount, ndr) to add their drives. // Normal usage of this module would be to add calls to New-PSDrive to your powershell profile. // We will now use this method to remove the old credential which may still be stored on the system without the user's knowledge. CredentialManager.ReadCredential("DropboxUserToken"); return drives; } #endregion #region Boolean Methods protected override bool ItemExists( string path ) { WriteDebug( "Invoking ItemExists({0})", path ); path = DropboxFileHelper.NormalizePath( path ); if ( IsRoot( path ) ) { return true; } return DropboxFileHelper.ItemExists( path, GetExistingChildItems ); } protected override bool IsItemContainer( string path ) { WriteDebug( "Invoking IsItemContainer({0})", path ); path = DropboxFileHelper.NormalizePath( path ); if ( IsRoot( path ) ) { return true; } bool result = true; MetaData data = null; try { data = Client.GetMetaData( path ); } catch ( DropboxException exception ) { switch ( exception.StatusCode ) { case HttpStatusCode.NotFound: result = false; break; default: throw; } } return result && data.Is_Dir; } #endregion #region Item Methods protected override bool HasChildItems( string path ) { WriteDebug( "Invoking HasChildItems({0})", path ); bool hasChildItems = false; path = DropboxFileHelper.NormalizePath( path ); Invoke( () => { MetaData metaData = Client.GetMetaData( path ); hasChildItems = metaData.Is_Dir && metaData.Contents != null && metaData.Contents.Count > 0; } ); return hasChildItems; } protected override void GetChildItems( string path, bool recurse ) { WriteDebug( "Invoking GetChildItems({0}, {1})", path, recurse ); foreach ( MetaData item in GetExistingChildItems( path ).OrderBy( x => !x.Is_Dir ).ThenBy( x => x.Name ) ) { WriteItemObject( item, item.Path, item.Is_Dir ); } } protected override void GetItem( string path ) { WriteDebug( "Invoking GetItem({0})", path ); MetaData item = null; path = DropboxFileHelper.NormalizePath( path ); Invoke( () => { item = Client.GetMetaData( path ); } ); WriteItemObject( item, item.Path, item.Is_Dir ); } protected override void CopyItem( string path, string copyPath, bool recurse ) { WriteDebug( "Invoking CopyItem({0}, {1}, {2})", path, copyPath, recurse ); path = DropboxFileHelper.NormalizePath( path ); copyPath = DropboxFileHelper.NormalizePath( copyPath ); MetaData result = Invoke( () => Client.Copy( path, copyPath ) ); WriteItemObject( result, copyPath, IsItemContainer( copyPath ) ); } protected override void MoveItem( string fromPath, string toPath ) { WriteDebug( "Invoking MoveItem({0}, {1}, {2})", fromPath, toPath ); fromPath = DropboxFileHelper.NormalizePath( fromPath ); toPath = DropboxFileHelper.NormalizePath( toPath ); MetaData result = Invoke( () => Client.Move( fromPath, toPath ) ); WriteItemObject( result, toPath, IsItemContainer( toPath ) ); } protected override void RemoveItem( string path, bool recurse ) { WriteDebug( "Invoking RemoveItem({0}, {1})", path, recurse ); MetaData result = null; path = DropboxFileHelper.NormalizePath( path ); Invoke( () => result = Client.Delete( path ) ); WriteItemObject( result, result.Path, result.Is_Dir ); } protected override void NewItem( string path, string itemTypeName, object newItemValue ) { UploadFile( path, itemTypeName, newItemValue ); } private void UploadFile( string path, string itemTypeName, object newItemValue ) { WriteDebug( "Invoking NewItem({0}, {1}, {2})", path, itemTypeName, newItemValue ); path = DropboxFileHelper.NormalizePath( path ); Action throwUnknownType = () => ThrowTerminatingError( new ArgumentException( @"The type is not a known type for the file system. Only ""file"" and ""directory"" can be specified.", nameof( itemTypeName ) ), ErrorId.ItemTypeNotValid ); if ( itemTypeName == null ) { throwUnknownType(); } // TODO: Verify that dropbox already checked the item doesn't exist and the path is valid. switch ( itemTypeName.ToLower() ) { case "directory": if ( ShouldProcess( path, $"New-Item: {itemTypeName}" ) ) { Invoke( () => { var result = Client.CreateFolder( path ); WriteItemObject( result, path, false ); } ); } break; case "file": if ( ShouldProcess( path, $"New-Item: {itemTypeName}" ) ) { string localFilePath = Path.GetTempFileName(); try { Invoke( () => { using ( FileStream stream = File.Open( localFilePath, FileMode.Open ) ) { Debug.Assert( stream != null, "stream != null" ); var result = Client.UploadFile( Path.GetDirectoryName( path ), Path.GetFileName( path ), stream ); WriteItemObject( result, path, false ); } } ); } finally { File.Delete( localFilePath ); } } break; default: throwUnknownType(); break; } } #endregion #region Helper Methods protected override string[] ExpandPath( string path ) { var pathInfo = DropboxFileHelper.GetPathInfo( path ); var items = GetExistingChildItems( pathInfo.Directory ); if ( items == null ) { return null; } var regexString = Regex.Escape( pathInfo.Name ).Replace( "\\*", ".*" ); var regex = new Regex( "^" + regexString + "$", RegexOptions.IgnoreCase ); var matchingItems = ( from item in items where regex.IsMatch( item.Name ) select pathInfo.Directory + "/" + item.Name ).ToArray(); return matchingItems.Any() ? matchingItems : null; } private IEnumerable<MetaData> GetExistingChildItems( string path ) { path = DropboxFileHelper.NormalizePath( path ); List<MetaData> results = null; Invoke( () => results = Client.GetMetaData( path ).Contents ); return results?.Where( item => !item.Is_Deleted ); } private void WriteDebug( string format, params object[] args ) { // string message = string.Format( format, args ); // base.WriteDebug( message ); } private void WriteObject(string obj) { WriteWarning(obj); } private static bool IsRoot( string path ) { return String.IsNullOrEmpty( path ); } #endregion #region Error Handling private void ThrowTerminatingError( Exception exception, ErrorId errorId, ErrorCategory errorCategory, object targetObject = null ) { ErrorRecord errorRecord = new ErrorRecord( exception, errorId.ToString(), errorCategory, targetObject ); ThrowTerminatingError( errorRecord ); } private void ThrowTerminatingError( ArgumentException exception, ErrorId errorId, object targetObject = null ) { ErrorRecord errorRecord = new ErrorRecord( exception, errorId.ToString(), ErrorCategory.InvalidArgument, targetObject ); ThrowTerminatingError( errorRecord ); } private void ThrowTerminatingError( DropboxException dropboxException, object targetObject = null ) { // TODO: Map exception.StatusCode to ErrorCategories if ( dropboxException.Response != null ) { try { dynamic errorData = JsonConvert.DeserializeObject( dropboxException.Response.Content ); string message; if ( errorData.error != null ) { // TODO: Figure out how to discover the properties on a JSON object. if ( errorData.Keys != null && errorData.error.path != null ) { message = errorData.error.path.Value; } else { message = errorData.error.ToString(); } } else { message = dropboxException.Response.Content; } // Attempt to enhance the message. FieldInfo fieldInfo = dropboxException.GetType().GetField( "_message", BindingFlags.NonPublic | BindingFlags.FlattenHierarchy | BindingFlags.Instance ); Debug.Assert( fieldInfo != null, "fieldInfo != null" ); fieldInfo.SetValue( dropboxException, message ); } catch ( SecurityException ) { /*Ignore if unsuccessful */ } } ErrorRecord errorRecord = new ErrorRecord( dropboxException, dropboxException.StatusCode.ToString(), ErrorCategory.InvalidOperation, targetObject ); ThrowTerminatingError( errorRecord ); } protected void WriteError( Exception exception, string errorId, ErrorCategory category, object targetObject = null ) { WriteError( new ErrorRecord( exception, errorId, category, "test" ) ); } private enum ErrorId { NoDriveAssociatedWithProvider, PSDriveInfoCannotBeNull, ItemTypeNotValid } #endregion Error Handling #region Invocation private void Invoke( Action func ) { Invoke( () => { func(); return true; } ); } private T Invoke<T>( Func<T> func ) { T result = default(T); if ( PSDriveInfo == null ) { ThrowTerminatingError( new InvalidOperationException( "There are currently no PSDrives created for this provider." ), ErrorId.NoDriveAssociatedWithProvider, ErrorCategory.InvalidOperation ); } else { try { result = func(); } catch ( DropboxException exception ) { ThrowTerminatingError( exception ); } } return result; } #endregion } } |