diff --git a/Src/Notion.Client/Api/ApiEndpoints.cs b/Src/Notion.Client/Api/ApiEndpoints.cs index 068e53db..fea18493 100644 --- a/Src/Notion.Client/Api/ApiEndpoints.cs +++ b/Src/Notion.Client/Api/ApiEndpoints.cs @@ -139,5 +139,10 @@ public static class AuthenticationUrls public static string IntrospectToken() => "/v1/oauth/introspect"; public static string RefreshToken() => "/v1/oauth/token"; } + + public static class FileUploadsApiUrls + { + public static string Create() => "/v1/file_uploads"; + } } } diff --git a/Src/Notion.Client/Api/FileUploads/Create/FileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/Create/FileUploadsClient.cs new file mode 100644 index 00000000..f60e22cc --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Create/FileUploadsClient.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Notion.Client +{ + public sealed partial class FileUploadsClient : IFileUploadsClient + { + public async Task CreateAsync( + CreateFileUploadRequest fileUploadObjectRequest, + CancellationToken cancellationToken = default) + { + if (fileUploadObjectRequest == null) + { + throw new ArgumentNullException(nameof(fileUploadObjectRequest)); + } + + ICreateFileUploadBodyParameters body = fileUploadObjectRequest; + + return await _restClient.PostAsync( + ApiEndpoints.FileUploadsApiUrls.Create(), + body, + cancellationToken: cancellationToken + ); + } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Create/Request/CreateFileUploadRequest.cs b/Src/Notion.Client/Api/FileUploads/Create/Request/CreateFileUploadRequest.cs new file mode 100644 index 00000000..6b5e4776 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Create/Request/CreateFileUploadRequest.cs @@ -0,0 +1,15 @@ +namespace Notion.Client +{ + public class CreateFileUploadRequest : ICreateFileUploadBodyParameters + { + public FileUploadMode Mode { get; set; } + + public string Filename { get; set; } + + public string ContentType { get; set; } + + public int? NumberOfParts { get; set; } + + public string ExternalUrl { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Create/Request/FileUploadMode.cs b/Src/Notion.Client/Api/FileUploads/Create/Request/FileUploadMode.cs new file mode 100644 index 00000000..c22715da --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Create/Request/FileUploadMode.cs @@ -0,0 +1,16 @@ +using System.Runtime.Serialization; + +namespace Notion.Client +{ + public enum FileUploadMode + { + [EnumMember(Value = "single_part")] + SinglePart, + + [EnumMember(Value = "multi_part")] + MultiPart, + + [EnumMember(Value = "external_url")] + ExternalUrl + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Create/Request/ICreateFileUploadBodyParameters.cs b/Src/Notion.Client/Api/FileUploads/Create/Request/ICreateFileUploadBodyParameters.cs new file mode 100644 index 00000000..38b21532 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Create/Request/ICreateFileUploadBodyParameters.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Notion.Client +{ + public interface ICreateFileUploadBodyParameters + { + /// + /// How the file is being sent. Use multi_part for files larger than 20MB. + /// Use external_url for files that are temporarily hosted publicly elsewhere. + /// Default is single_part. + /// + [JsonProperty("mode")] + [JsonConverter(typeof(StringEnumConverter))] + FileUploadMode Mode { get; } + + /// + /// Name of the file to be created. Required when mode is multi_part or external_url. + /// Otherwise optional, and used to override the filename. Must include an extension, or have one inferred + /// from the content_type parameter. + /// + [JsonProperty("filename")] + string Filename { get; } + + /// + /// MIME type of the file to be created. Recommended when sending the file in multiple parts. + /// Must match the content type of the file that's sent, and the extension of the filename parameter if any. + /// + [JsonProperty("content_type")] + string ContentType { get; } + + /// + /// When mode is multi_part, the number of parts you are uploading. + /// Must be between 1 and 1,000. This must match the number of parts as well as the final part_number you send. + /// + [JsonProperty("number_of_parts")] + int? NumberOfParts { get; } + + /// + /// When mode is external_url, provide the HTTPS URL of a publicly accessible file to import into your workspace. + /// + [JsonProperty("external_url")] + string ExternalUrl { get; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/Create/Response/CreateFileUploadResponse.cs b/Src/Notion.Client/Api/FileUploads/Create/Response/CreateFileUploadResponse.cs new file mode 100644 index 00000000..13fbc105 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/Create/Response/CreateFileUploadResponse.cs @@ -0,0 +1,6 @@ +namespace Notion.Client +{ + public class CreateFileUploadResponse : FileObjectResponse + { + } +} diff --git a/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportError.cs b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportError.cs new file mode 100644 index 00000000..a55ad6a9 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportError.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class FileImportError + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("parameter")] + public string Parameter { get; set; } + + [JsonProperty("status_code")] + public int? StatusCode { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportErrorResult.cs b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportErrorResult.cs new file mode 100644 index 00000000..d72c82fe --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportErrorResult.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class FileImportErrorResult : FileImportResult + { + public override string Type => "error"; + + [JsonProperty("error")] + public FileImportError Error { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportResult.cs b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportResult.cs new file mode 100644 index 00000000..a855f787 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportResult.cs @@ -0,0 +1,21 @@ +using System; +using JsonSubTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Notion.Client +{ + [JsonConverter(typeof(JsonSubtypes), "type")] + [JsonSubtypes.KnownSubTypeAttribute(typeof(FileImportSuccessResult), "success")] + [JsonSubtypes.KnownSubTypeAttribute(typeof(FileImportErrorResult), "error")] + [JsonSubtypes.FallBackSubTypeAttribute(typeof(FileImportResult))] + public abstract class FileImportResult + { + [JsonProperty("type")] + public virtual string Type { get; set; } + + [JsonProperty("imported_time")] + [JsonConverter(typeof(IsoDateTimeConverter))] + public DateTime ImportedTime { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportSuccessResult.cs b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportSuccessResult.cs new file mode 100644 index 00000000..bef29d05 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileImportSuccessResult.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class FileImportSuccessResult : FileImportResult + { + public override string Type => "success"; + + [JsonProperty("success")] + public Dictionary Success { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileUploadObjectResponse.cs b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileUploadObjectResponse.cs new file mode 100644 index 00000000..39285c3b --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/FileUploadObjectResponse/FileUploadObjectResponse.cs @@ -0,0 +1,47 @@ +using System; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class FileObjectResponse : IObject + { + public string Id { get; set; } + public ObjectType Object => ObjectType.FileUpload; + + [JsonProperty("created_time")] + public DateTime CreatedTime { get; set; } + + [JsonProperty("created_by")] + public PartialUser CreatedBy { get; set; } + + [JsonProperty("last_edited_time")] + public DateTime LastEditedTime { get; set; } + + [JsonProperty("archived")] + public bool Archived { get; set; } + + [JsonProperty("expiry_time")] + public DateTime? ExpiryTime { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("filename")] + public string FileName { get; set; } + + [JsonProperty("content_type")] + public string ContentType { get; set; } + + [JsonProperty("content_length")] + public long? ContentLength { get; set; } + + [JsonProperty("upload_url")] + public string UploadUrl { get; set; } + + [JsonProperty("complete_url")] + public string CompleteUrl { get; set; } + + [JsonProperty("file_import_result")] + public FileImportResult FileImportResult { get; set; } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/FileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/FileUploadsClient.cs new file mode 100644 index 00000000..26293257 --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/FileUploadsClient.cs @@ -0,0 +1,12 @@ +namespace Notion.Client +{ + public sealed partial class FileUploadsClient : IFileUploadsClient + { + private readonly IRestClient _restClient; + + public FileUploadsClient(IRestClient restClient) + { + _restClient = restClient; + } + } +} diff --git a/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs b/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs new file mode 100644 index 00000000..cca82a9f --- /dev/null +++ b/Src/Notion.Client/Api/FileUploads/IFileUploadsClient.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Notion.Client +{ + public interface IFileUploadsClient + { + /// + /// Use this API to initiate the process of uploading a file to your Notion workspace. + /// + /// + /// + /// + Task CreateAsync( + CreateFileUploadRequest fileUploadObjectRequest, + CancellationToken cancellationToken = default + ); + } +} \ No newline at end of file diff --git a/Src/Notion.Client/Models/ObjectType.cs b/Src/Notion.Client/Models/ObjectType.cs index 4a9dc77d..61188a31 100644 --- a/Src/Notion.Client/Models/ObjectType.cs +++ b/Src/Notion.Client/Models/ObjectType.cs @@ -17,6 +17,9 @@ public enum ObjectType User, [EnumMember(Value = "comment")] - Comment + Comment, + + [EnumMember(Value = "file_upload")] + FileUpload, } } diff --git a/Src/Notion.Client/NotionClient.cs b/Src/Notion.Client/NotionClient.cs index 2fc6f479..6f21ccf7 100644 --- a/Src/Notion.Client/NotionClient.cs +++ b/Src/Notion.Client/NotionClient.cs @@ -18,6 +18,7 @@ public interface INotionClient IBlocksClient Blocks { get; } ICommentsClient Comments { get; } + IFileUploadsClient FileUploads { get; } IRestClient RestClient { get; } } @@ -32,7 +33,8 @@ public NotionClient( ISearchClient search, ICommentsClient comments, IBlocksClient blocks, - IAuthenticationClient authenticationClient) + IAuthenticationClient authenticationClient, + IFileUploadsClient fileUploadsClient) { RestClient = restClient; Users = users; @@ -42,6 +44,7 @@ public NotionClient( Comments = comments; Blocks = blocks; AuthenticationClient = authenticationClient; + FileUploads = fileUploadsClient; } public IAuthenticationClient AuthenticationClient { get; } @@ -58,6 +61,8 @@ public NotionClient( public ICommentsClient Comments { get; } + public IFileUploadsClient FileUploads { get; } + public IRestClient RestClient { get; } } } diff --git a/Src/Notion.Client/NotionClientFactory.cs b/Src/Notion.Client/NotionClientFactory.cs index e66f0781..2fc9acac 100644 --- a/Src/Notion.Client/NotionClientFactory.cs +++ b/Src/Notion.Client/NotionClientFactory.cs @@ -15,6 +15,7 @@ public static NotionClient Create(ClientOptions options) , new CommentsClient(restClient) , new BlocksClient(restClient) , new AuthenticationClient(restClient) + , new FileUploadsClient(restClient) ); } } diff --git a/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs b/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs new file mode 100644 index 00000000..306f550c --- /dev/null +++ b/Test/Notion.IntegrationTests/FIleUploadsClientTests.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Notion.Client; +using Xunit; + +namespace Notion.IntegrationTests +{ + public class FileUploadsClientTests : IntegrationTestBase + { + [Fact] + public async Task CreateAsync() + { + // Arrange + var request = new CreateFileUploadRequest + { + Mode = FileUploadMode.ExternalUrl, + ExternalUrl = "https://unsplash.com/photos/hOhlYhAiizc/download?ixid=M3wxMjA3fDB8MXxhbGx8fHx8fHx8fHwxNzYwMTkxNzc3fA&force=true", + Filename = "sample-image.jpg", + }; + + // Act + var response = await Client.FileUploads.CreateAsync(request); + + // Assert + Assert.NotNull(response); + Assert.NotNull(response.Status); + Assert.Equal("sample-image.jpg", response.FileName); + } + } +} \ No newline at end of file diff --git a/Test/Notion.IntegrationTests/IntegrationTestBase.cs b/Test/Notion.IntegrationTests/IntegrationTestBase.cs index 34707ff1..1d537f02 100644 --- a/Test/Notion.IntegrationTests/IntegrationTestBase.cs +++ b/Test/Notion.IntegrationTests/IntegrationTestBase.cs @@ -18,7 +18,7 @@ protected IntegrationTestBase() ParentPageId = GetEnvironmentVariableRequired("NOTION_PARENT_PAGE_ID"); ParentDatabaseId = GetEnvironmentVariableRequired("NOTION_PARENT_DATABASE_ID"); } - + protected static string GetEnvironmentVariableRequired(string envName) { return Environment.GetEnvironmentVariable(envName) ??