Как создать несколько файлов tar.gz, переопределив определенные файлы для каждой среды?

Как создать несколько файлов tar.gz, переопределив определенные файлы для каждой среды?

У меня есть корневая папка Productsи куча подпапок внутри нее. В каждой из этих подпапок сейчас куча файлов. Просто для простоты я придумал названия подпапок как folder{number}и имена файлов как, files{number}.jsonно в целом у них разные названия.

Обычно у меня внутри корневой папки имеется 20 различных подпапок, и в каждой подпапке содержится около 30 файлов максимум.

(Рисунок 1)

Products
├── folder1
│   ├── files1.json
│   ├── files2.json
│   └── files3.json
├── folder2
│   ├── files4.json
│   ├── files5.json
│   └── files6.json
└── folder3
    ├── files10.json
    ├── files7.json
    ├── files8.json
    └── files9.json

Теперь я сжимаю все это в tar.gzфайл, выполнив следующую команду:

tar cvzf ./products.tgz Products

Вопрос:-

Я получил новый дизайн, как показано ниже, где каждая подпапка внутри Productsкорневой папки содержит три папки среды — dev, stageи prod.

(фигура 2)

Products
├── folder1
│   ├── dev
│   │   └── files1.json
│   ├── files1.json
│   ├── files2.json
│   ├── files3.json
│   ├── prod
│   │   └── files1.json
│   └── stage
│       └── files1.json
├── folder2
│   ├── dev
│   │   └── files5.json
│   ├── files4.json
│   ├── files5.json
│   ├── files6.json
│   ├── prod
│   │   └── files5.json
│   └── stage
│       └── files5.json
└── folder3
    ├── files10.json
    ├── files7.json
    ├── files8.json
    └── files9.json

Например - Внутри folder1подпапки есть еще три подпапки dev, stageи prodи то же самое для других подпапок folder2и folder3. Каждая из них dev, stageи prodподпапка внутри folder{number}подпапки будут иметь файлы, которые для них переопределены.

Теперь мне нужно сгенерировать три разных tar.gzфайла — по одному для каждого dev, stageи prodна основе указанной выше структуры.

  • Какие бы файлы у меня ни были внутри dev, stageони prodтакже перезапишут файлы своих подпапок, если они присутствуют в их подпапках (folder1, folder2 или folder3).
  • Таким образом, если files1.jsonприсутствует в folder1подпапке и тот же файл также присутствует внутри любой из dev, stageто prodпри упаковке мне нужно использовать то, что присутствует в их папке окружения, и переопределить файлы их подпапок, в противном случае просто использовать то, что присутствует в их подпапках.

В итоге у меня будет 3 разных структуры, подобных этой — одна для dev, одна для stageи еще одна для prod, где папка 1 (или 2 и 3) будет содержать файлы в соответствии с тем, что у меня есть в их среде в качестве первого предпочтения, поскольку они переопределяются, и другие файлы, которые не переопределяются.

(рисунок 3)

Products
├── folder1
│   ├── files1.json
│   ├── files2.json
│   └── files3.json
├── folder2
│   ├── files4.json
│   ├── files5.json
│   └── files6.json
└── folder3
    ├── files10.json
    ├── files7.json
    ├── files8.json
    └── files9.json

И мне нужно сгенерировать products-dev.gz, products-stage.gzи products-prod.gzиз figure 2которых будут данные, подобные, figure 3но специфичные для каждой среды. Единственное отличие в том, что каждая подпапка folder1 (2 или 3) будет иметь файлы, которые переопределяются для них как первое предпочтение из их конкретной папки среды, а остальные будут использовать только из их подпапки.

Можно ли это сделать с помощью некоторых команд Linux? Единственное, что меня смущает, это как перезаписать определенные файлы среды внутри определенной подпапки, а затем сгенерировать tar.gzв них 3 разных файла.

Обновлять:

Также рассмотрите такие случаи, как следующие:

Products
├── folder1
│   ├── dev
│   │   ├── files1.json
│   │   └── files5.json
│   ├── files1.json
│   ├── files2.json
│   ├── files3.json
│   ├── prod
│   │   ├── files10.json
│   │   └── files1.json
│   └── stage
│       └── files1.json
├── folder2
│   ├── dev
│   ├── prod
│   └── stage
└── folder3
    ├── dev
    ├── prod
    └── stage

Как вы можете видеть folder2, folder3есть папки переопределения среды, но в них нет никаких файлов, поэтому в этом случае я хочу сгенерировать пустой folder2и folder3также в каждой среде определенный tar.gzфайл.

решение1

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

В качестве однострочного кода, хотя и немного длинного, можно сделать следующее для одной итерации, т.е. для одного каталога «environments»:

(r=Products; e=stage; (find -- "$r" -regextype posix-extended -maxdepth 2 \( -regex '^[^/]+(/[^/]+)?' -o ! -type d \) -print0; find -- "$r" -mindepth 1 -path "$r/*/$e/*" -print0) | tar --null --no-recursion -czf "$r-$e.tgz" -T- --transform=s'%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%')

разбить на части, чтобы лучше рассмотреть:

(
    r=Products; e=stage
    (
        find -- "$r" -regextype posix-extended -maxdepth 2 \( -regex '^[^/]+(/[^/]+)?' -o ! -type d \) -print0
        find -- "$r" -mindepth 1 -path "$r/*/$e/*" -print0
    ) \
        | tar --null --no-recursion -czf "$r-$e.tgz" -T- \
            --transform=s'%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%'
)

На что следует обратить внимание:

  1. он показывает синтаксис инструментов GNU. Для BSD findвы должны заменить -regextype posix-extendedна just -Eи для BSD tarвы должны заменить --no-recursionна just, -nа также --transform=s(<- обратите внимание на финал s) на just-s
  2. Для простоты демонстрации предполагается, что фрагмент запускается из каталога, содержащего Products, и использует пользовательскую $eпеременную для имени каталога "environments" для архивации, в то время как $r является просто вспомогательной переменной с коротким именем, содержащей Productsимя
  3. он заключен в скобки, что делает его подоболочкой, просто чтобы не засорять вашу оболочку, $rи $eесли вы запускаете его из командной строки
  4. он не копирует и не ссылается на исходные файлы, он обрабатывает любые допустимые имена файлов, у него нет ограничений по памяти, и он может обрабатывать любое количество имен; единственное предположение касается первых двух уровней иерархии каталогов, поскольку любой каталог, расположенный непосредственно под первым уровнем, считается каталогом «окружения» и, таким образом, игнорируется (за исключением указанного в $e).

Вы можете просто заключить этот фрагмент в for e in dev prod stage; do ...; doneцикл оболочки и просто продолжить (возможно, убрав внешние скобки и окружив ими весь forцикл).

Плюс в том, что он довольно короткий и относительно простой.

Недостатком является то, что он всегда архивирует такжевсе the переопределенофайлы (т. е. базовые), трюк в том, что двойные findкоманды tarсначала подают файлы, которые должны быть переопределены, и, следовательно, во время извлечения они будут перезаписаны переопределяющими файлами (т. е. файлами, специфичными для "сред"). Это приводит к тому, что архив большего размера занимает больше времени как при создании, так и при извлечении, и может быть нежелательным в зависимости от того, можно ли пренебречь такими "издержками" или нет.

Этот трубопровод, описанный в прозе, выглядит следующим образом:

  1. (кроме внешних скобок и вспомогательных переменных)
  2. первая findкоманда создает только список неспецифичных файлов (и ведущих каталогов в соответствии с вашим обновлением), тогда как вторая findсоздает только список всех файлов, специфичных для среды.
  3. эти две findкоманды заключены в скобки, так что обе их выходные данные подаются в канал tarпоследовательно
  4. tarсчитывает такой канал, чтобы получить имена файлов, и помещает эти файлы в архив, одновременно --transformудаляя компонент «environments» (если он присутствует) из пути к каждому файлу
  5. эти две findкоманды разделены, а не являются одной, и они запускаются одна за другой, так что неспецифические файлы создаются (для tarиспользования) до специфичных для среды файлов, что позволяет использовать трюк, описанный мной ранее.

Чтобы избежать накладных расходов, связанных с включениемвсегда всефайлы нам нужна дополнительная сложность, чтобы действительно очистить переопределенные файлы. Один из способов может быть таким:

# still a pipeline, but this time I won't even pretend it to be a one-liner

(
r=Products; e=stage; LC_ALL=C
find -- "$r" -regextype posix-extended \( -path "$r/*/$e/*" -o \( -regex '^([^/]+/){2}[^/]+' ! -type d \) -o -regex '^[^/]+(/[^/]+)?' \) -print0 \
    | sed -zE '\%^(([^/]+/){2})([^/]+/)%s%%0/\3\1%;t;s%^%1//%' \
    | sort -zt/ -k 3 -k 1,1n \
    | sort -zut/ -k 3 \
    | sed -zE 's%^[01]/(([^/]+/)|/)(([^/]+/?){2})%\3\2%' \
    | tar --null --no-recursion -czf "$r-$e.tgz" -T- \
        --transform=s'%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%'
)

Следует отметить несколько моментов:

  1. все, что мы сказали ранее относительно синтаксисов GNU и BSD для findи tarприменимо и здесь
  2. как и предыдущее решение, оно не имеет никаких ограничений, кроме предположения о первых двух уровнях иерархии каталогов.
  3. Я использую GNU sedздесь для работы с вводом-выводом с разделителями-нулями (опция -z), но вы можете легко заменить эти две sedкоманды, например, while read ...циклом оболочки (требуется Bash версии 3 или выше) или другим языком, в котором вы уверены, единственная рекомендация — чтобы инструмент, который вы используете, мог обрабатывать ввод-вывод с разделителями-нулями (например, GNU gawkможет это сделать); см. ниже замену с использованием циклов Bash.
  4. Я использую здесь один сингл find, так как не полагаюсь на какое-либо подразумеваемое поведениеtar
  5. Команды sedманипулируют списком имен, прокладывая путь для sortкоманд
  6. в частности, первый sedперемещает имя «среды» в начало пути, также добавляя к нему префикс в виде вспомогательного 0числа, просто чтобы сортировать его перед файлами, не относящимися к средам, так как я добавляю к ним префикс в виде лидирующего числа 1с целью сортировки
  7. Такая подготовка нормализует список имен в «глазах» команд sort, делая все имена без имени «окружения» и все имеющие одинаковое количество полей, разделенных косой чертой, в начале, что важно для sortопределений ключей
  8. первый sortприменяет сортировку, основанную сначала на именах файлов, таким образом помещая одинаковые имена рядом друг с другом, а затем на числовом значении 0или 1, как отмечено ранее командой sed, таким образом гарантируя, что любой файл, специфичный для "сред", если он присутствует, будет располагаться перед своим неспецифическим аналогом
  9. второй sortобъединяет (опция -u) имена файлов, оставляя только первое из дублирующихся имен, которое из-за предыдущего переупорядочивания всегда является файлом, специфичным для «среды», если оно присутствует
  10. наконец, второй sedотменяет то, что было сделано первым, тем самым изменяя имена файлов для tarархива

Если вам интересно изучить промежуточные части такого длинного конвейера, имейте в виду, что все они работают снуль-разделенные имена, и поэтому плохо отображаются на экране. Вы можете передать любой из промежуточных выводов (т. е. убрать хотя бы tar) в любезность, tr '\0' '\n'чтобы показать вывод, понятный человеку, просто помните, что имена файлов с переводами строк будут занимать две строки на экране.

Можно было бы внести несколько улучшений, например, сделав его полностью параметризованной функцией/скриптом или, например, автоматически определяя любое произвольное имя для каталогов «environments», как показано ниже:

Важный: обратите внимание на комментарии, так как они могут быть не очень хорошо восприняты интерактивной оболочкой

(
export r=Products LC_ALL=C
cd -- "$r/.." || exit
# make arguments out of all directories lying at the second level of the hierarchy
set -- "$r"/*/*/
# then expand all such paths found, take their basenames only, uniquify them, and pass them along xargs down to a Bash pipeline the same as above
printf %s\\0 "${@#*/*/}" \
    | sort -zu \
    | xargs -0I{} sh -c '
e="${1%/}"
echo --- "$e" ---
find -- "$r" -regextype posix-extended \( -path "$r/*/$e/*" -o \( -regex '\''^([^/]+/){2}[^/]+'\'' ! -type d \) -o -regex '\''^[^/]+(/[^/]+)?'\'' \) -print0 \
    | sed -zE '\''\%^(([^/]+/){2})([^/]+/)%s%%0/\3\1%;t;s%^%1//%'\'' \
    | sort -zt/ -k 3 -k 1,1n \
    | sort -zut/ -k 3 \
    | sed -zE '\''s%^[01]/(([^/]+/)|/)(([^/]+/?){2})%\3\2%'\'' \
    | tar --null --no-recursion -czf "$r-$e.tgz" -T- \
        --transform=s'\''%^\(\([^/]\{1,\}/\)\{2\}\)[^/]\{1,\}/%\1%'\''
' packetizer {}
)

Пример замены первой sedкоманды с помощью цикла Bash:

(IFS=/; while read -ra parts -d $'\0'; do
    if [ "${#parts[@]}" -gt 3 ]; then
        env="${parts[2]}"; unset parts[2]
        printf 0/%s/%s\\0 "$env" "${parts[*]}"
    else
        printf 1//%s\\0 "${parts[*]}"
    fi
done)

Для второй sedкоманды:

(IFS=/; while read -ra parts -d $'\0'; do
    printf %s "${parts[*]:2:2}" "/${parts[1]:+${parts[1]}/}" "${parts[*]:4}"
    printf \\0
done)

Оба фрагмента требуют окружающих скобок, чтобы они могли быть вставленными заменами для соответствующих sed команд в конвейере выше, и, конечно, sh -cчасть после них xargsнеобходимо преобразовать в bash -c.

решение2

Общее решение

  1. Сделайте копию дерева каталогов. Сделайте жесткие ссылки на файлы для экономии места.
  2. Измените копию. (В случае жестких ссылок вам нужно знать, что вы можете сделать безопасно. См. ниже.)
  3. Сохраните копию в архиве.
  4. Удалить копию.
  5. При необходимости повторите (изменяя по-другому).

Пример

Ограничения:

  • в этом примере используются не-POSIX параметры (проверено на Debian 10),
  • он делает некоторые предположения о дереве каталогов,
  • может произойти сбой, если файлов слишком много.

Относитесь к этому как к доказательству концепции и адаптируйте его к своим потребностям.

  1. Создание копии

    cdв родительский каталог Products. Этот каталог Productsи все, что в нем находится, должны принадлежать к одной файловой системе. Создайте временный каталог и пересоздайте Productsтам:

    mkdir -p tmp
    cp -la Products/ tmp/
    
  2. Изменение копии

    Файлы в двух деревьях каталогов жестко связаны. Если вы измените ихсодержанието вы измените исходные данные. Операции, которые изменяют информацию, хранящуюся в каталогах, безопасны, они не изменят исходные данные, если будут выполнены в другом дереве. Это:

    • удаление файлов,
    • переименование файлов,
    • перемещение файлов (включая перемещение файла поверх другого файла с помощью mv),
    • создание полностью независимых файлов.

    В вашем случае для каждого каталога с именем devна нужной глубине переместите его содержимое на один уровень вверх:

    cd tmp/Products
    dname=dev
    find . -mindepth 2 -maxdepth 2 -type d -name "$dname" -exec sh -c 'cd "$1" && mv -f -- * ../' sh {} \;
    

    Примечания:

    • mv -- * ../склонен к argument list too long,
    • по умолчанию *не соответствует dotfiles.

    Затем удалите каталоги:

    find . -mindepth 2 -maxdepth 2 -type d -exec rm -rf {} +
    

    Обратите внимание, что это удаляет теперь пустые devи ненужные prod, stage;илюбой другой каталог на этой глубине.

  3. Архивация копии

    # still in tmp/Products because of the previous step
    cd ..
    tar cvzf "products-$dname.tgz" Products
    
  4. Удаление копии

    # now in tmp because of the previous step
    rm -rf Products
    
  5. Повторяющийся

    Вернитесь в нужный каталог и начните заново, на этот раз с dname=stage; и так далее.


Пример сценария (быстрый и грубый)

#!/bin/bash

dir=Products
[ -d "$dir" ] || exit 1
mkdir -p tmp

for dname in dev prod stage; do
(
   cp -la "$dir" tmp/
   cd "tmp/$dir"
   [ "$?" -eq 0 ] || exit 1
   find . -mindepth 2 -maxdepth 2 -type d -name "$dname" -exec sh -c 'cd "$1" && mv -f -- * ../' sh {} \;
   find . -mindepth 2 -maxdepth 2 -type d -exec rm -rf {} +
   cd ..
   [ "$?" -eq 0 ] || exit 1
   tar cvzf "${dir,,}-$dname.tgz" "$dir"
   rm -rf "$dir" || exit 1
) || exit "$?"
done

решение3

Я сделал это немного более общим и работаю над нетривиальными именами файлов, фактически не меняя исходные каталоги.

Productsуказывается как аргумент. Ключевые слова dev prod stageжестко закодированы внутри скрипта (но могут быть легко изменены)

Примечание: это специфично для GNU --transformи -print0 -zявляется расширением

запустить скрипт
./script Products

#!/bin/sh

# environment
subdirs="dev prod stage"

# script requires arguments
[ -n "$1" ] || exit 1

# remove trailing /
while [ ${i:-0} -le $# ]
  do
    i=$((i+1))
    dir="$1"
    while [ "${dir#"${dir%?}"}" = "/" ]
      do
        dir="${dir%/}"
    done
    set -- "$@" "$dir"
    shift
done

# search string
for sub in $subdirs
  do
    [ -n "$search" ] && search="$search -o -name $sub" || search="( -name $sub"
done
search="$search )"

# GNU specific zero terminated handling for non-trivial directory names
excludes="$excludes $(find -L "$@" -type d $search -print0 | sed -z 's,[^/]*/,*/,g' | sort -z | uniq -z | xargs -0 printf '--exclude=%s\n')"

# for each argument
for dir in "$@"
  do
    # for each environment
    [ -e "$dir" ] || continue
    for sub in $subdirs
      do
        # exclude other subdirs
        exclude=$(echo "$excludes" | grep -v "$sub")

#        # exclude files that exist in subdir (at least stable against newlines and spaces in file names)
#        include=$(echo "$excludes" | grep "$sub" | cut -d= -f2)
#        [ -n "$include" ] && files=$(find $include -mindepth 1 -maxdepth 1 -print0 | tr '\n[[:space:]]' '?' | sed -z "s,/$sub/,/," | xargs -0 printf '--exclude=%s\n')
#        exclude="$exclude $files"

        # create tarball archive
        archive="${dir##*/}-${sub}.tgz"
        [ -f "$archive" ] && echo "WARNING: '$archive' is overwritten"
        tar --transform "s,/$sub$,," --transform "s,/$sub/,/," $exclude -czhf "$archive" "$dir"
    done
done

Вы можете заметить дубликаты внутри архива. tarБудет рекурсивно спускаться по каталогам, при восстановлении будут восстановлены более глубокие файлы.перезаписатьфайлы в родительском каталоге

Однако это требует дальнейшего тестирования на соответствие поведению (в этом я не уверен). Правильным способом было бы exlude files1.json+, files5.jsonк сожалению, -Xне работает с--null

Если вы не доверяете такому поведению или не хотите, чтобы в архивах были дубликаты файлов, вы можете добавить исключение для простых имен файлов.раскомментироватькод выше tar. В именах файлов допускаются символы новой строки и пробелы, но они будут исключены с помощью подстановочного знака ?в шаблоне исключения, что теоретически может исключить больше файлов, чем ожидалось (если есть похожие файлы, соответствующие этому шаблону)

вы можете поместить echoперед tarи вы увидите, что скрипт генерирует следующие команды

tar --transform 's,/dev$,,' --transform 's,/dev/,/,' --exclude=*/*/prod --exclude=*/*/stage -czhf Products-dev.tgz Products
tar --transform 's,/prod$,,' --transform 's,/prod/,/,' --exclude=*/*/dev --exclude=*/*/stage -czhf Products-prod.tgz Products
tar --transform 's,/stage$,,' --transform 's,/stage/,/,' --exclude=*/*/dev --exclude=*/*/prod -czhf Products-stage.tgz Products

Связанный контент