diff --git a/Src/Notion.Client/Models/File/FileObject.cs b/Src/Notion.Client/Models/File/FileObject.cs index 0ac30a6c..1b51e35a 100644 --- a/Src/Notion.Client/Models/File/FileObject.cs +++ b/Src/Notion.Client/Models/File/FileObject.cs @@ -6,6 +6,7 @@ namespace Notion.Client { [JsonConverter(typeof(JsonSubtypes), "type")] [JsonSubtypes.KnownSubTypeAttribute(typeof(UploadedFile), "file")] + [JsonSubtypes.KnownSubTypeAttribute(typeof(UploadingFile), "file_upload")] [JsonSubtypes.KnownSubTypeAttribute(typeof(ExternalFile), "external")] public abstract class FileObject : IPageIcon { diff --git a/Src/Notion.Client/Models/File/FileObjectWithName.cs b/Src/Notion.Client/Models/File/FileObjectWithName.cs index 855a5e0f..c1261b59 100644 --- a/Src/Notion.Client/Models/File/FileObjectWithName.cs +++ b/Src/Notion.Client/Models/File/FileObjectWithName.cs @@ -6,6 +6,7 @@ namespace Notion.Client [JsonConverter(typeof(JsonSubtypes), "type")] [JsonSubtypes.KnownSubTypeAttribute(typeof(UploadedFileWithName), "file")] [JsonSubtypes.KnownSubTypeAttribute(typeof(ExternalFileWithName), "external")] + [JsonSubtypes.KnownSubTypeAttribute(typeof(FileUploadWithName), "file_upload")] public abstract class FileObjectWithName { [JsonProperty("type")] diff --git a/Src/Notion.Client/Models/File/FileUploadWithName.cs b/Src/Notion.Client/Models/File/FileUploadWithName.cs new file mode 100644 index 00000000..a3e93a8f --- /dev/null +++ b/Src/Notion.Client/Models/File/FileUploadWithName.cs @@ -0,0 +1,19 @@ +using System; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class FileUploadWithName : FileObjectWithName + { + public override string Type => "file_upload"; + + [JsonProperty("file_upload")] + public Info FileUpload { get; set; } + + public class Info + { + [JsonProperty("id")] + public Guid Id { get; set; } + } + } +} diff --git a/Src/Notion.Client/Models/File/UploadingFile.cs b/Src/Notion.Client/Models/File/UploadingFile.cs new file mode 100644 index 00000000..a78b421c --- /dev/null +++ b/Src/Notion.Client/Models/File/UploadingFile.cs @@ -0,0 +1,19 @@ +using System; +using Newtonsoft.Json; + +namespace Notion.Client +{ + public class UploadingFile : FileObject + { + public override string Type => "file_upload"; + + [JsonProperty("file_upload")] + public Info FileUpload { get; set; } + + public class Info + { + [JsonProperty("id")] + public Guid Id { get; set; } + } + } +} diff --git a/Src/Notion.Client/RestClient/IRestClient.cs b/Src/Notion.Client/RestClient/IRestClient.cs index b8ffe5b3..f675340c 100644 --- a/Src/Notion.Client/RestClient/IRestClient.cs +++ b/Src/Notion.Client/RestClient/IRestClient.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; @@ -35,5 +36,8 @@ Task DeleteAsync( IDictionary queryParams = null, IDictionary headers = null, CancellationToken cancellationToken = default); + + Task Upload(string filePath, JsonSerializerSettings serializerSettings = null); + Task Upload(Stream stream, string filename, JsonSerializerSettings serializerSettings = null); } } diff --git a/Src/Notion.Client/RestClient/RestClient.cs b/Src/Notion.Client/RestClient/RestClient.cs index 59a848f7..f8850fd5 100644 --- a/Src/Notion.Client/RestClient/RestClient.cs +++ b/Src/Notion.Client/RestClient/RestClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -91,6 +92,74 @@ public async Task DeleteAsync( await SendAsync(uri, HttpMethod.Delete, queryParams, headers, null, cancellationToken); } + public async Task Upload(string filePath, JsonSerializerSettings serializerSettings = null) + { + var fileStream = File.OpenRead(filePath); + return await Upload(fileStream, Path.GetFileName(filePath), serializerSettings); + } + + public async Task Upload(Stream stream, string filename, JsonSerializerSettings serializerSettings = null) + { + var response = await this.PostAsync("https://api.notion.com/v1/file_uploads", + new UploadRequest {Mode = "single_part"}); + + using (var formData = new MultipartFormDataContent()) + { + var fileContent = new StreamContent(stream); + + var mimeMapping = new Dictionary + { + {".jpg", "image/jpeg"}, + {".jpeg", "image/jpeg"}, + {".png", "image/png"}, + {".tif", "image/tiff"}, + {".tiff", "image/tiff"}, + {".gif", "image/gif"}, + {".svg", "image/svg+xml"} + }; + + if (mimeMapping.TryGetValue(Path.GetExtension(filename).ToLower(), out var mapping)) + { + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mapping); + } + formData.Add(fileContent, "file", filename); + + var uploadResponse = await this.SendAsync(response.UploadUrl, HttpMethod.Post, + attachContent: message => + { + message.Content = formData; + }); + return await uploadResponse.ParseStreamAsync(serializerSettings); + } + } + + public class UploadRequest + { + public string Mode { get; set; } + } + + public class UploadResponse + { + public Guid Id { get; set; } + public string Object { get; set; } + [JsonProperty("created_time")] + public DateTime CreatedTime { get; set; } + [JsonProperty("last_edited_time")] + public DateTime LastEditedTime { get; set; } + [JsonProperty("expiry_time")] + public DateTime ExpiryTime { get; set; } + [JsonProperty("upload_url")] + public string UploadUrl { get; set; } + public bool Archived { get; set; } + public string Status { get; set; } + public string Filename { get; set; } + [JsonProperty("content_type")] + public string ContentType { get; set; } + [JsonProperty("request_id")] + public Guid RequestId { get; set; } + } + + private static ClientOptions MergeOptions(ClientOptions options) { return new ClientOptions diff --git a/Test/Notion.IntegrationTests/PageClientTests.cs b/Test/Notion.IntegrationTests/PageClientTests.cs index 4938bcfa..fab980ce 100644 --- a/Test/Notion.IntegrationTests/PageClientTests.cs +++ b/Test/Notion.IntegrationTests/PageClientTests.cs @@ -53,7 +53,8 @@ public async Task InitializeAsync() } } }, - { "Number", new NumberPropertySchema { Number = new Number { Format = "number" } } } + { "Number", new NumberPropertySchema { Number = new Number { Format = "number" } } }, + {"Profile picture", new FilePropertySchema() { Files = new Dictionary()}} }, Parent = new ParentPageInput { PageId = _page.Id } }; @@ -179,6 +180,64 @@ public async Task Test_RetrievePagePropertyItemAsync() titleProperty.Title.PlainText.Should().Be("Test Page Title"); }); } + + [Fact] + public async Task Test_CanUploadAndSetProfilePicture() + { + // Arrange + var upload = await Client.RestClient.Upload("/Users/kkuepper/Pictures/Scan.jpeg"); + + // Act + var pagesCreateParameters = PagesCreateParametersBuilder + .Create(new DatabaseParentInput {DatabaseId = _database.Id}) + .AddProperty("Name", + new TitlePropertyValue + { + Title = new List + { + new RichTextText {Text = new Text {Content = "Test Page Title"}} + } + }) + .AddProperty("Number", new NumberPropertyValue {Number = 123}) + .AddProperty("Profile picture", + new FilesPropertyValue + { + Files = new List + { + new FileUploadWithName {FileUpload = new FileUploadWithName.Info {Id = upload.Id}} + } + } + ) + .AddPageContent(new ImageBlock + { + Image = new UploadingFile {FileUpload = new UploadingFile.Info {Id = upload.Id}} + }) + .Build(); + + var page = await Client.Pages.CreateAsync(pagesCreateParameters); + + var property = await Client.Pages.RetrievePagePropertyItemAsync(new RetrievePropertyItemParameters + { + PageId = page.Id, + PropertyId = "Profile picture" + }); + + // Assert + property.Should().NotBeNull(); + property.Should().BeOfType(); + + var listProperty = (FilesPropertyItem)property; + + listProperty.Type.Should().NotBeNull(); + + listProperty.Files.Should().SatisfyRespectively(p => + { + p.Should().BeOfType(); + var fileWithName = (UploadedFileWithName)p; + + fileWithName.Name.Should().Be("Scan.jpeg"); + }); + } [Fact] public async Task Test_UpdatePageProperty_with_date_as_null()