Description:
  Using GitLab's API v4, it is possible to change a project's avatar.
  However the request needs to be submitted via PUT with `Content-Type: multipart/form-data` header set.
  REF: https://docs.gitlab.com/ee/api/projects.html#upload-a-project-avatar
Author: g0t mi1k <have.you.g0tmi1k@gmail.com>
Last-Update: Mon, 23 Jan 2023 06:10:02 +0000
Forwarded: https://github.com/bluefeet/GitLab-API-v4/pull/54
---
 author/generate.pl              |  6 ++++++
 author/sections/projects.yml    |  4 ++++
 lib/GitLab/API/v4.pm            | 26 ++++++++++++++++++++++
 lib/GitLab/API/v4/RESTClient.pm | 48 ++++++++++++++++++++++++++++++-----------
 4 files changed, 72 insertions(+), 12 deletions(-)

diff --git a/author/generate.pl b/author/generate.pl
index 76096ed..341dbb3 100755
--- a/author/generate.pl
+++ b/author/generate.pl
@@ -121,6 +121,12 @@ foreach my $section_name (keys %$section_pack) {
             print "    \$options->{$params_key} = \$params if defined \$params;\n";
         }
 
+        my @code;
+        @code = split /\n/, $endpoint->{code} if $endpoint->{code};
+        foreach my $line (@code) {
+            print "    $line\n";
+        }
+
         print '    ';
         print 'return ' if $return;
         print "\$self->_call_rest_client( '$verb', '$path', [\@_], \$options );\n";
diff --git a/author/sections/projects.yml b/author/sections/projects.yml
index 5ed0990..4e149f9 100644
--- a/author/sections/projects.yml
+++ b/author/sections/projects.yml
@@ -6,6 +6,10 @@
 - create_project: project = POST projects?
 - create_project_for_user: POST projects/user/:user_id?
 - edit_project: PUT projects/:project_id?
+- method: edit_project_multipart
+  spec: PUT projects/:project_id?
+  note: The request will have "multipart/form-data" header set for uploading files.
+  code: $options->{content}->{file} = $params;
 - fork_project: POST projects/:project_id/fork?
 - project_forks: forks = GET projects/:project_id/forks?
 - start_project: project = POST projects/:project_id/star
diff --git a/lib/GitLab/API/v4.pm b/lib/GitLab/API/v4.pm
index 87ba571..6ad83ce 100644
--- a/lib/GitLab/API/v4.pm
+++ b/lib/GitLab/API/v4.pm
@@ -7340,6 +7340,32 @@ sub edit_project {
     return;
 }
 
+=item edit_project_multipart
+
+    $api->edit_project_multipart(
+        $project_id,
+        \%params,
+    );
+
+Sends a C<PUT> request to C<projects/:project_id>.
+
+The request will have "multipart/form-data" header set for uploading files.
+=cut
+
+sub edit_project_multipart {
+    my $self = shift;
+    croak 'edit_project_multipart must be called with 1 to 2 arguments' if @_ < 1 or @_ > 2;
+    croak 'The #1 argument ($project_id) to edit_project_multipart must be a scalar' if ref($_[0]) or (!defined $_[0]);
+    croak 'The last argument (\%params) to edit_project_multipart must be a hash ref' if defined($_[1]) and ref($_[1]) ne 'HASH';
+    my $params = (@_ == 2) ? pop() : undef;
+    my $options = {};
+    $options->{decode} = 0;
+    $options->{content} = $params if defined $params;
+    $options->{content}->{file} = $params;
+    $self->_call_rest_client( 'PUT', 'projects/:project_id', [@_], $options );
+    return;
+}
+
 =item fork_project
 
     $api->fork_project(
diff --git a/lib/GitLab/API/v4/RESTClient.pm b/lib/GitLab/API/v4/RESTClient.pm
index 471dae5..045663b 100644
--- a/lib/GitLab/API/v4/RESTClient.pm
+++ b/lib/GitLab/API/v4/RESTClient.pm
@@ -162,10 +162,18 @@ sub request {
 
     my $req_method = 'request';
     my $req = [ $verb, $url, $options ];
+    my $boundary;
 
-    if ($verb eq 'POST' and ref($content) eq 'HASH' and $content->{file}) {
+    if (($verb eq 'POST' or $verb eq 'PUT' ) and ref($content) eq 'HASH' and $content->{file}) {
         $content = { %$content };
-        my $file = path( delete $content->{file} );
+        my $file = delete $content->{file};
+
+        my $key = (keys %$file)[0]
+            if (ref $file);
+
+        $file = (ref $file)
+            ? path( $file->{$key} )
+            : path( $file );
 
         unless (-f $file and -r $file) {
             local $Carp::Internal{ 'GitLab::API::v4' } = 1;
@@ -183,18 +191,34 @@ sub request {
             },
         };
 
-        $req->[0] = $req->[1]; # Replace method with url.
-        $req->[1] = $data; # Put data where url was.
-        # So, req went from [$verb,$url,$options] to [$url,$data,$options],
-        # per the post_multipart interface.
-
-        $req_method = 'post_multipart';
-        $content = undef if ! %$content;
+        if ($verb eq 'POST') {
+            $req->[0] = $req->[1]; # Replace method with url.
+            $req->[1] = $data; # Put data where url was.
+            # So, req went from [$verb,$url,$options] to [$url,$data,$options],
+            # per the post_multipart interface.
+
+            $req_method = 'post_multipart';
+            $content = undef if ! %$content;
+        } elsif ($verb eq 'PUT') {
+            $boundary .= sprintf("%x", rand 16) for 1..16;
+            $content = <<"EOL";
+--------------------------$boundary
+Content-Disposition: form-data; name="$key"; filename="$data->{file}->{filename}"
+
+$data->{file}->{content}
+--------------------------$boundary--
+EOL
+        }
     }
 
-    if (ref $content) {
-        $content = $self->json->encode( $content );
-        $headers->{'content-type'} = 'application/json';
+    if (defined $boundary or ref $content) {
+        $content = $self->json->encode( $content )
+          if (ref $content);
+
+      $headers->{'content-type'} = (defined $boundary)
+          ? "multipart/form-data; boundary=------------------------$boundary"
+          : 'application/json';
+
         $headers->{'content-length'} = length( $content );
     }
 
