diff --git a/src/ClientServer/Config/Constants.cs b/src/ClientServer/Config/Constants.cs
index da100848cb02360eee3cc267894fc4139c5ccf38..f111c21b86aea4e451c2f3be82211e2009781912 100644
--- a/src/ClientServer/Config/Constants.cs
+++ b/src/ClientServer/Config/Constants.cs
@@ -13,7 +13,7 @@ namespace ClientServer.Helpers
     /// </summary>
     public static class Constants
     {
-        public static string VersionString = "2.25.2";
+        public static string VersionString = "2.26.0";
 
         /// <summary>
         /// this is only set once at program.cs!!
diff --git a/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs b/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs
index 8f2bebc83d8dd1b4eb868c13e21b1bd225eb0694..6b4a521a254129403e311d580eab8aeb4841fc84 100644
--- a/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs
+++ b/src/ClientServer/Controllers/Core/Exercises/DoExerciseController.cs
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Diagnostics.Contracts;
 using System.IO;
+using System.IO.Compression;
 using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
@@ -2017,7 +2018,7 @@ namespace ClientServer.Controllers.Core.Exercises
 
             if ((await _context.PLangs.FirstOrDefaultAsync(p => p.Id == solutionForBackend.PLangId)) == null)
             {
-                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "p lang found")));
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "p lang not found")));
                 return;
             }
 
@@ -2239,6 +2240,402 @@ namespace ClientServer.Controllers.Core.Exercises
                 );
         }
 
+        /// <summary>
+        /// saves or creates the user solution for the given exercise and release
+        /// 
+        /// we use the first plang of the participation of the user
+        /// we only know we get files in a zip file, so we have to match the template files by name
+        /// file extension must be known by the plang
+        /// 
+        /// see https://stackoverflow.com/questions/77718442/unable-to-send-a-multipart-request-with-an-image-and-json-using-fromform
+        /// <para></para>
+        /// IF this method returns an InvalidRequest and a string, then a file name was invalid... the string contains the invalid file name
+        /// </summary>
+        /// <param name="generatedCode">the code for the exercise release</param>
+        /// <param name="shouldClearAllTestResults">let the frontend decide if the code has changed... (e.g. editor has unsaved changes)</param>
+        /// <param name="updateSolutionData">multipart form data with a zip file as content (see type for file name)</param>
+        [HttpPost("save/upload/{generatedCode}/{shouldClearAllTestResults}")]
+        public async Task SaveCodeFilesFromUpload(string generatedCode, bool shouldClearAllTestResults,
+            [FromForm] SaveFilesFromZip updateSolutionData)
+        {
+            if (!await base.IsLoggedIn()) return;
+
+            int userId = GetUserId();
+
+            if (updateSolutionData == null)
+            {
+                await
+                    Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest, "no files found")));
+                return;
+            }
+
+
+            //we use the first lang in the participation as plang
+
+            ExerciseReleaseWithUserAsParticipation oldExerciseReleaseWithUserAsParticipation =
+                    await _context.ExerciseReleaseWithUserAsParticipations
+                        .Where(p => p.ExerciseRelease.GeneratedCode == generatedCode && p.UserId == userId)
+                        .Include(p => p.Solutions)
+                        .ThenInclude(p => p.SolutionFiles)
+                        .ThenInclude(solutionFile => solutionFile.TemplateFile)
+                        .Include(p => p.Solutions)
+                        .ThenInclude(p => p.CustomTestResults)
+                        .Include(p => p.Solutions)
+                        .ThenInclude(p => p.TestResults)
+                        .Include(p => p.ExerciseRelease)
+                        .Include(p => p.ExerciseRelease.Exercise)
+                        .ThenInclude(exercise => exercise.CodeTemplates)
+                        .ThenInclude(p => p.TemplateFiles)
+                        .Include(exerciseReleaseWithUserAsParticipation =>
+                            exerciseReleaseWithUserAsParticipation.Solutions)
+                        .ThenInclude(solution => solution.PLang)
+                        .FirstOrDefaultAsync()
+                ;
+
+            ExerciseRelease release = oldExerciseReleaseWithUserAsParticipation?.ExerciseRelease;
+
+
+            if (oldExerciseReleaseWithUserAsParticipation?.ExerciseRelease == null)
+            {
+                //this should not happen because when accessing the exercise (getting exercise) then the participation is ensured...
+
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "release not found")));
+                return;
+            }
+
+            if (release.Exercise == null)
+            {
+                await
+                    Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound,
+                        "exercise was not found")));
+                return;
+            }
+
+            if (release.Exercise.IsPermanentlyLocked)
+            {
+                await
+                    Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                        "exercise is permanently locked")));
+                return;
+            }
+
+            //check if available working time is up
+            if (ReleaseHelper.IsTimeUp(oldExerciseReleaseWithUserAsParticipation,
+                oldExerciseReleaseWithUserAsParticipation.ExerciseRelease, this))
+            {
+                return;
+            }
+
+            if (oldExerciseReleaseWithUserAsParticipation.ExerciseRelease.Exercise.IsOnlyVisibleToOwner)
+            {
+                //then only the owner can access the exercise
+                if (userId != oldExerciseReleaseWithUserAsParticipation.ExerciseRelease.Exercise.UserId)
+                {
+                    await
+                        Response.WriteAsync(
+                            Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                                "exercise is only accessible by owner")));
+                    return;
+                }
+            }
+
+
+            //we don't get info about the plang, use the last one
+            var lastEditedPLangId = oldExerciseReleaseWithUserAsParticipation.LastEditedPLangId;
+
+
+            //use template to get readonly files and check if youser can modify files
+            CodeTemplate oldCodeTemplateForPLang = release.Exercise.CodeTemplates
+                .FirstOrDefault(p => p.PLangId == lastEditedPLangId);
+
+
+            if (oldCodeTemplateForPLang == null)
+            {
+                //no template exists...
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "no template found")));
+                return;
+            }
+
+
+            //search for solution for the given p lang
+            Solution oldSolution =
+                oldExerciseReleaseWithUserAsParticipation.Solutions
+                    .FirstOrDefault(
+                        p => p.PLangId == lastEditedPLangId);
+
+
+            if (oldSolution == null)
+            {
+                //this should not happen because when accessing the exercise (getting exercise) then the participation (and solution) is ensured...
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "solution not found")));
+                return;
+            }
+
+            SolutionDoExerciseFullBase tmpSolution = new SolutionDoExerciseFullBase();
+            var tmpSolutionFiles = new List<SolutionFileDoExerciseFullBase>();
+
+
+            //to avoid using wrong files (e.g. osx), ignore files that start with a "." "_"
+
+            var oldSolutionPlang = oldSolution.PLang;
+            
+            var validFileExtensions = oldSolutionPlang.FileExtensionsWithDot.Split(",", StringSplitOptions.RemoveEmptyEntries);
+
+            SolutionFileDoExerciseFullBase tmpMainFile = null;
+            var _filesCount = -1;
+            using (MemoryStream memoryStream = new MemoryStream())
+            {
+                await updateSolutionData.SolutionFiles.CopyToAsync(memoryStream);
+                try
+                {
+                using (ZipArchive zipArchive = new ZipArchive(memoryStream, ZipArchiveMode.Read))
+                {
+                    for (int i = 0; i < zipArchive.Entries.Count; i++)
+                    {
+                        var entry = zipArchive.Entries[i];
+
+                        if (string.IsNullOrEmpty(entry.Name))
+                        {
+                            //directory
+                            continue;
+                        }
+
+                        // / is default but some apps use file system separator
+                        if (entry.FullName.StartsWith(".") 
+                                                         || entry.FullName.StartsWith("_")
+                                                         || entry.Name.StartsWith(".") 
+                                                         || entry.Name.StartsWith("_")
+                                                         )
+                        {
+                            //only use first level, not inside dirs
+                            continue;
+                        }
+                        
+                        if (!validFileExtensions.Any(p => entry.Name.EndsWith(p)))
+                        {
+                            //invalid extension, ignore file
+                            continue;
+                        }
+                        
+                        using StreamReader stream = new StreamReader(entry.Open(), Encoding.Default);
+                        string fileContent = await stream.ReadToEndAsync();
+                        string fileName = entry.Name.Trim();
+
+                        TemplateFile templateFile =
+                            oldCodeTemplateForPLang.TemplateFiles.FirstOrDefault(p =>
+                                p.FileNameWithExtension.Trim() == fileName);
+                        
+                        var tmpSolutionFile = new SolutionFileDoExerciseFullBase()
+                        {
+                            Id = _filesCount--,
+                            Content = fileContent,
+                            FileNameWithExtension = fileName,
+                            TemplateFileId = null,
+                        };
+                        
+                        var oldSolutionFile = oldSolution.SolutionFiles.FirstOrDefault(p => 
+                            p.FileNameWithExtension.Trim() == fileName);
+
+                        if (oldSolutionFile != null)
+                        {
+                            //old file
+                            tmpSolutionFile.Id = oldSolutionFile.Id;
+                            //this is ok to set here because OverwriteSolution
+                            //also uses the template from the old solution
+                            templateFile = oldSolutionFile.TemplateFile;
+                        }
+                            
+                        if (templateFile == null)
+                        {
+                            //new file
+                            //check if the user was allowed to create files
+                            if (release.Exercise.CanUserCreateFiles)
+                            {
+                                if (Files.ValidFileName(fileName) == false)
+                                {
+                                    //not a valid file name...
+                                    await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                                        "invalid filename: " + fileName.Trim())));
+                                    return;
+                                }
+                            }
+                            else
+                            {
+                                //ignore file because user was not allowed to create...
+                                continue;
+                            }
+                            
+                        }
+                        else
+                        {
+                            //old file (has template)
+                            tmpSolutionFile.TemplateFileId = templateFile.Id;
+                            tmpSolutionFile.IsContentVisibleForUser = templateFile.IsContentVisibleForUser;
+                            tmpSolutionFile.IsEditableByUser = templateFile.IsEditableByUser;
+                            tmpSolutionFile.UiIsDisplayed = true;
+
+                            if (oldCodeTemplateForPLang.MainFileId == templateFile.Id)
+                            {
+                                tmpMainFile = tmpSolutionFile;
+                            }
+                        }
+                            
+                        tmpSolutionFiles.Add(tmpSolutionFile);
+                    }
+                }
+                }
+                catch (Exception e)
+                {
+                    await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                        "no or invalid zip provided")));
+                    return;
+                }
+               
+            }
+
+
+            if (tmpSolutionFiles.Count == 0)
+            {
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                    "no files found")));
+                return;
+            }
+            
+            if (tmpMainFile == null)
+            {
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                    "main file missing")));
+                return;
+            }
+
+            tmpSolution.MainFileId = tmpMainFile.Id;
+            tmpSolution.SolutionFiles = tmpSolutionFiles;
+            
+            List<Tuple<SolutionFileDoExerciseFullBase, SolutionFile>> fileAssociation = null;
+            Tuple<Solution, SolutionFile> mainClassFileTuple = null;
+
+            #region handle solution update
+
+            string invalidFileName = OverwriteSolution(tmpSolution,
+                oldSolution, oldCodeTemplateForPLang,
+                release, shouldClearAllTestResults, ref mainClassFileTuple, out fileAssociation);
+
+            //invalidate
+            tmpSolution = null;
+            
+            if (invalidFileName != null)
+            {
+                await Response.WriteAsync(Jc.Serialize(new BasicResponse(ResponseCode.InvalidRequest,
+                    "invalid filename: " + invalidFileName)));
+                return;
+            }
+
+            oldSolution.LastEditingIpAddress = NetworkHelper.GetIpAddress(HttpContext);
+
+            oldExerciseReleaseWithUserAsParticipation.LastUpdatedAt = DateTimeHelper.GetUtcNow();
+
+            #endregion
+
+
+            //check if main file really got set
+            if (mainClassFileTuple == null)
+            {
+                await
+                    Response.WriteAsync(
+                        Jc.Serialize(new BasicResponse(ResponseCode.NotFound, "no main file found")));
+                return;
+            }
+
+            try
+            {
+                using (var transaction = _context.Database.BeginTransaction())
+                {
+                    try
+                    {
+                        //use a transaction to ensure the main file is already 
+
+                        //save the solution changes
+                        await _context.SaveChangesAsync();
+
+                        //always save in case user has an invalid solution
+                        mainClassFileTuple.Item1.MainFile = mainClassFileTuple.Item2;
+                        await _context.SaveChangesAsync();
+
+                        transaction.Commit();
+                    }
+                    catch (Exception ex)
+                    {
+                        transaction.Rollback();
+
+                        await base.HandleDbError(ex);
+                        return;
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                //see https://www.npgsql.org/doc/transactions.html
+                await base.HandleDbError(e);
+                return;
+            }
+
+            string errorMsg =
+                UserSolutionHelper.ReplaceReadonlyFiles(oldSolution, oldCodeTemplateForPLang, true, false);
+
+            if (string.IsNullOrEmpty(errorMsg) == false)
+            {
+                await
+                    Response.WriteAsync(
+                        Jc.Serialize(new BasicResponse(ResponseCode.ServerError,
+                            "could not replace readonly or hiden files")));
+                return;
+            }
+
+            var solutionVersionForFrontend = new SolutionDoExerciseFullBase()
+            {
+                UserId = oldSolution.UserId,
+                ReleaseId = oldSolution.ExerciseReleaseId,
+                PLangId = oldSolution.PLangId,
+                LastUpdatedAt = oldSolution.LastUpdatedAt,
+                // ReSharper disable once PossibleInvalidOperationException //this should be set or checked in OverwriteSolutionVersion or SaveNewSolutionVersion
+                MainFileId = oldSolution.MainFileId ?? 0,
+                SolutionFiles =
+                    oldSolution.SolutionFiles.Select(
+                        p => new SolutionFileDoExerciseFullBase() //solutionVersionForBackend
+                        {
+                            Id = p.Id,
+                            Content = p.Content,
+                            UiIsDisplayed = p.IsDisplayed,
+                            //when there is no template file the user created the file and is allowed to edit it
+                            IsEditableByUser = (p.TemplateFile == null || p.TemplateFile.IsEditableByUser),
+                            FileNameWithExtension = p.FileNameWithExtension,
+                            TemplateFileId = p.TemplateFileId,
+                            DisplayIndex = p.DisplayIndex,
+                            IsContentVisibleForUser = p.TemplateFile?.IsContentVisibleForUser ?? true,
+                        }).ToList()
+            };
+
+
+            var solutionVersionTupleForFrontend = new SolutionTupleFromBackend
+            {
+                Solution = solutionVersionForFrontend,
+                FileIdAssociationMap = fileAssociation.Select(p => new FileIdAssociation()
+                {
+                    FrontendFileId = p.Item1.Id,
+                    BackendFileId = p.Item2.Id
+                }).ToList()
+            };
+
+
+            await
+                Response.WriteAsync(
+                    Jc.Serialize(new BasicResponseWithData<SolutionTupleFromBackend>(ResponseCode.Ok,
+                        "saved solution version",
+                        solutionVersionTupleForFrontend))
+                );
+        }
+
+
 
         [HttpPost("reset/{generatedCode}/{pLangId}")]
         public async Task ResetCode(string generatedCode, int pLangId)
@@ -4337,5 +4734,17 @@ namespace ClientServer.Controllers.Core.Exercises
 
     #endregion
 
+    
+    #region only for api
+
+    //see https://stackoverflow.com/questions/77718442/unable-to-send-a-multipart-request-with-an-image-and-json-using-fromform
+    public class SaveFilesFromZip
+    {
+        [FromForm(Name = "solutionFiles")]
+        public IFormFile SolutionFiles { get; set; }
+    }
+    
+    #endregion
+    
     #endregion
 }