Архив рубрики ‘C#’

Поток HEIC картинок загружается в OneDrive. Для передачи их далее в другую систему понадобилось конвертировать их в JPG.

Из коробки Power Automate кстати тоже может это делать(«Convert File» + «Create File»), но файл не должен быть больше 2Mb..

Вариант который обрабатывает любые файлы:

1-1-2023-07-26_174225

1) «Get file content» в Power Automate уже возвращает результат в base64, поэтому нам не нужно ничего ковертировать. единственное что..
2) его результат содержит не только содержимое файла, но и его имя и тип. Так чтобы добраться именно до самого содержимого] — нужно использовать такую строку:

body('Get_file_content')['$content']

Тогда мы получим сам код картинки, который и будем конвертировать.

3) При создании Azure функции ей будет присвоен адрес и код. он добавляется к адресу в поле URI и по нему происходит авторизация(там где “?code=fFyK..”) . В «Body» отсылается ID, имя файла и полученный выше контент в base64.

Со стороны Azure функция может выглядеть к примеру так:

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using ImageMagick;

namespace HeicToJpgWebservice
{
    public static class Function1
    {
        [FunctionName("HeicToJpgWebservice")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,  ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            // Get request body
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);

            string base64String = data?.content;
            string id = data?.id;
            string name = data?.name;

            log.LogInformation($"Processing {id}, filename is: {name}");

            byte[] heicAsByteArray = null;
            string jpgAsBase64 = string.Empty, responseMessage = string.Empty;

            try
            {
                heicAsByteArray = Convert.FromBase64String(base64String);

                using (MagickImage image = new MagickImage(heicAsByteArray))
                {
                    // convert
                    image.Format = MagickFormat.Jpeg;

                    // return result Jpg as base64
                    jpgAsBase64 = image.ToBase64();

                    responseMessage = string.IsNullOrEmpty(jpgAsBase64) ? $"Error during conversion: id = {id}, name = {name}" : jpgAsBase64;
                }
            }
            catch (Exception ex)
            {
                log.LogWarning($"Error during conversion: id = {id}, name = {name}, exception: {ex?.InnerException?.Message}");
                return new BadRequestObjectResult($"Error during conversion: id = {id}, name = {name}, exception: {ex?.InnerException?.InnerException}, {ex?.InnerException?.Message}");
            }

            return new OkObjectResult(responseMessage);
        }
    }
}

Собственно id и имя файла особо не нужны, они передаются на всякий случай чтобы отловить потенциальную ошибку в конвертации. Функция получает содержимое HEIC файла в base64, конвертирует через библиотеку Magick.NET-Q16-AnyCPU (ставится через Nuget), и возвращает обратно тоже base64, но уже формата JPEG.

После HTTP вызова функции выполняется правый блок (“Scope create file” внизу):

В развёрнутом виде:

Со стороны Power Automate остаётся этот base64 подхватить и создать на его основе файл в другой папке, на этот раз с расширением *.jpg. Сконвертированное значение лежит в «body(‘HTTP’)», цифра 1 выше.

Правда чтобы создать файл — в «File Content» нужно отдать это значение в binary (цифра 2 на рисунке выше):

base64ToBinary(outputs('Compose_result_from_Azure_function'))

p.s.: Для того чтобы флов срабатывал только для *.heic файлов — в самом первом шаге можно использовать trigger condition:

@endswith(tolower(triggerOutputs()?['body/{FilenameWithExtension}']), '.heic')

  8-1-2023-07-26_173737

8-2-2023-07-26_173803

p.p.s.: Также подобный подход можно использовать например чтобы вытаскивать картинки из форм infopath, правда функция само собой будет другая (картинки в SharePoint Infopath лежат в строке, где первые байты занимает название файла).

reSP — great Resharper plugin from SubPoint Solutions.

Makes Your SharePoint solutions code more stable and robust:
http://subpointsolutions.com/resp/

я обновил Sharepoint EventHandlerExplorer 2013/2016, утилиту которая позволяет управлять обработчиками событий без использования SharePoint-фич. Позволяет привязывать ресиверы к вебам, спискам/библиотекам SharePoint, а также к контент-тайпам:

Скачать

Описание:

Фичи позволяют привязывать обработчики только к известным типам контента(хрестоматийный пример – список “Announcements”):

<Receivers ListTemplateId="104">

Но такое декларативное развёртывание действует на весь scope и не позволяет к примеру привязать два обработчика к разным Announcement-спискам. Также не позволяет регистрировать обработчик для списка с неизвестным Id.

Изначально первая версия EventHandlerExplorer была создана Patrick Tisseghem и работала с третьей версией WSS. В Windows 2012(в .NET 4) появился новый глобальный кеш сборок, и исчезла возможность “перетягивать” мышью DLL-ки в GAC. Вместо drag-n-drop теперь рекомендуется использовать powershell:

[System.Reflection.Assembly]::Load('System.EnterpriseServices, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b03f5f7f11d50a3a');
$publish = New-Object System.EnterpriseServices.Internal.Publish;
$publish.GacInstall('$fullpath');

я добавил обёртку для развёртывания через powershell и поддержку 2013 и 2016 версий SharePoint on-premise

Есть вебчасть, содержащая внутри себя меню PeopleEditor. Столкнулся с тем что для анонимных пользователей она недоступна:

control is not available

Спасибо Karel Hájek, который раскопал рефлектором что дело в дополнительной проверке, которая происходит на этапе рендеринга. Для отображения не хватает пермишна “BrowseUserInfo”, который по умолчанию выключен для анонимных юзеров, а веб-интерфейс не позволяет его выдать.

does_user_have_permissions

Karel предлагает подредактировать базу данных контента и разрешить анонимные запросы в Active Directory.

Намного проще это сделать через Powershell-скрипт, тем более что редактирование базы контента напрямую это очень bad-practice 🙂

Подключаем консоль, получим текущие разрешения и добавим к ним SPBasePermissions.BrowseUserInfo:

$sharePointSnapin = Get-PSSnapin | Where-Object { $_.Name -eq "Microsoft.SharePoint.PowerShell"}
if($sharePointSnapin -eq $null)
{
 Add-PSSnapin Microsoft.SharePoint.PowerShell 
}


$web = Get-SPWeb http://portal/web/

$web.AnonymousPermMask64;
# output: ViewListItems, ViewVersions, ViewFormPages, Open, ViewPages, UseClientIntegration


$web.AnonymousPermMask64 = "$rights, BrowseUserInfo";
$web.Update();

Теперь PeoplePicker рендерится для всех пользователей(для того чтобы он ещё и работал надо включить запросы в AD):

people_picker_rendered

Есть система багтреккинга, сделанная на Sharepoint с использованием Infopath-форм.

Формы имеют несколько копий и в зависимости от принадлежности сотрудника в AD, ему подставляется своя копия и люди из разных отделов могут редактировать только свои поля:

image

Задача:

После экспорта узла из вида пропало поле «Статуса». То есть на форме оно есть, а в колонках видов и настройках списка уже нет.

Решение:

— Переопубликовать форму в Infopath Designer(колонка должна появиться, но без значений).

— Обновить все элементы списка, чтобы значения появились в видах SharePoint.

Программно это можно сделать например так

  using (SPSite site = new SPSite("http://server/webName "))
 using (SPWeb web = site.OpenWeb())
 {
 SPList list = web.GetList("http://server/webname/libName/");
 foreach (SPListItem item in list.Items)
 {
 try
 {
 Console.Write("Updating " + item.ID.ToString());
 item.SystemUpdate();
 Console.WriteLine(".. Ok.");
 }
 catch (Exception ex)
 {
 Console.WriteLine(ex.ToString());
 }
 }
 } 

или через Powershell:

$web = Get-SPWeb -identity "http://server/webName"
 
$list = $web.Lists["listName"] 

$list.Items | ForEach-Object { Write-Host "Updating " $_.Name; $_.SystemUpdate(); }

$web.Dispose()

p.s.: при этом пользовательские виды с участием этих “пропавших и возвращённых” полей возможно прийдётся пересоздать

В интернетах есть несколько вариантов как к примеру обновить элемент без старта привязанных к нему событий, но они либо не очень работают, либо работают в определённых контекстах. Вот ниже способ, который работает при обращении к элементам даже из консольного приложения.

Это extension-класс для SPListItem. Основа это пост на stackoverflow , я лишь добавил от себя ещё пару функций для работы с файлами — добавление файла и функцию CopyTo, для «тихого» копирования файла.

using Microsoft.SharePoint;

public static class SPListItemExtensions
{
    /// <summary>
    /// Provides ability to update list item without firing event receiver.
    /// </summary>
    /// <param name="item">list item</param>
    /// <param name="doNotFireEvents">Disables firing event receiver while updating item.</param>
    public static void Update(this SPListItem item, bool doNotFireEvents)
    {
        SPItemEventReceiverHandling rh = new SPItemEventReceiverHandling();
        if (doNotFireEvents)
        {
            try
            {
                rh.DisableEventFiring();
                item.Update();
            }
            finally
            {
                rh.EnableEventFiring();
            }
        }
        else
        {
            item.Update();
        }
    }

    /// <summary>
    /// Provides ability to update list item without firing event receiver.
    /// </summary>
    public static void SystemUpdate(this SPListItem item, bool incrementListItemVersion, bool doNotFireEvents)
    {
        SPItemEventReceiverHandling rh = new SPItemEventReceiverHandling();
        if (doNotFireEvents)
        {
            try
            {
                rh.DisableEventFiring();
                item.SystemUpdate(incrementListItemVersion);
            }
            finally
            {
                rh.EnableEventFiring();
            }
        }
        else
        {
            item.SystemUpdate(incrementListItemVersion);
        }
    }

    /// <summary>
    /// Provides ability to copy file from list item without firing event receiver.
    /// </summary>
    public static void CopyTo(this SPFile file, string strNewUrl, bool bOverWrite, bool doNotFireEvents)
    {
        SPItemEventReceiverHandling rh = new SPItemEventReceiverHandling();
        if (doNotFireEvents)
        {
            try
            {
                rh.DisableEventFiring();
                file.CopyTo(strNewUrl, bOverWrite);
            }
            finally
            {
                rh.EnableEventFiring();
            }
        }
        else
        {
            file.CopyTo(strNewUrl, bOverWrite);
        }
    }


    /// <summary>
    /// Provides ability to update list item without firing event receiver.
    /// </summary>
    public static void SystemUpdate(this SPListItem item, bool doNotFireEvents)
    {
        SPItemEventReceiverHandling rh = new SPItemEventReceiverHandling();
        if (doNotFireEvents)
        {
            try
            {
                rh.DisableEventFiring();
                item.SystemUpdate();
            }
            finally
            {
                rh.EnableEventFiring();
            }
        }
        else
        {
            item.SystemUpdate();
        }
    }

    /// <summary>
    /// Provides ability to add file to sharepoint library without firing event receiver.
    /// </summary>
    public static SPFile AddFile(SPFileCollection spFileCollection, string destPathToFile, byte[] binFileData, bool overwrite, bool doNotFireEvents)
    {
        SPItemEventReceiverHandling rh = new SPItemEventReceiverHandling();
        if (doNotFireEvents)
        {
            try
            {
                rh.DisableEventFiring();
                SPFile addedFile = spFileCollection.Add(destPathToFile, binFileData, overwrite);
                return addedFile;
            }
            finally
            {
                rh.EnableEventFiring();
            }
        }
        else
        {
            SPFile addedFile = spFileCollection.Add(destPathToFile, binFileData, overwrite);
            return addedFile;
        }
    }

    private class SPItemEventReceiverHandling : SPItemEventReceiver
    {
        public SPItemEventReceiverHandling() { }

        new public void DisableEventFiring()
        {
#pragma warning disable 612,618
            base.DisableEventFiring();
#pragma warning restore 612,618
        }

        new public void EnableEventFiring()
        {
#pragma warning disable 612,618
            base.EnableEventFiring();
#pragma warning restore 612,618
        }
    }
}