Bagaimana saya dapat memeriksa apakah file dapat dibuat atau dipotong / ditimpa dalam bash?


15

Pengguna memanggil skrip saya dengan jalur file yang akan dibuat atau ditimpa pada beberapa titik dalam skrip, seperti foo.sh file.txtatau foo.sh dir/file.txt.

Perilaku create-or-overwrite mirip dengan persyaratan untuk meletakkan file di sisi kanan >operator redirect output, atau meneruskannya sebagai argumen untuk tee(pada kenyataannya, meneruskannya sebagai argumen teeadalah apa yang saya lakukan ).

Sebelum saya masuk ke nyali script, saya ingin melakukan pemeriksaan yang masuk akal apakah file tersebut dapat dibuat / ditimpa, tetapi tidak benar-benar membuatnya. Pemeriksaan ini tidak harus sempurna, dan ya saya menyadari bahwa situasinya dapat berubah antara cek dan titik di mana file tersebut sebenarnya ditulis - tetapi di sini saya OK dengan solusi jenis upaya terbaik sehingga saya dapat menyelamatkan lebih awal dalam hal path file tidak valid.

Contoh alasan file tidak dapat dibuat:

  • file berisi komponen direktori, seperti dir/file.txttetapi direktori dirtidak ada
  • pengguna tidak memiliki izin menulis di direktori yang ditentukan (atau CWD jika tidak ada direktori yang ditentukan

Ya, saya menyadari bahwa memeriksa izin "di muka" bukan The UNIX Way ™ , melainkan saya harus mencoba operasi dan meminta pengampunan nanti. Namun dalam skrip khusus saya, ini mengarah pada pengalaman pengguna yang buruk dan saya tidak dapat mengubah komponen yang bertanggung jawab.


Apakah argumen akan selalu didasarkan pada direktori saat ini atau bisakah pengguna menentukan path lengkap?
Jesse_b

@ Jesse_b - Saya kira pengguna dapat menentukan path absolut seperti /foo/bar/file.txt. Pada dasarnya saya melewati jalan untuk teemenyukai tee $OUT_FILEtempat OUT_FILEdilewatkan pada baris perintah. Itu harus "hanya bekerja" dengan jalur absolut dan relatif, bukan?
BeeOnRope

@BeeOnRope, tidak, setidaknya Anda membutuhkannya tee -- "$OUT_FILE". Bagaimana jika file tersebut sudah ada atau ada tetapi bukan file biasa (direktori, symlink, fifo)?
Stéphane Chazelas

@ StéphaneChazelas - baik saya menggunakan tee "${OUT_FILE}.tmp". Jika file sudah ada, teetimpa, yang merupakan perilaku yang diinginkan dalam kasus ini. Jika itu direktori, teeakan gagal (saya pikir). symlink Saya tidak yakin 100%?
BeeOnRope

Jawaban:


11

Tes yang jelas adalah:

if touch /path/to/file; then
    : it can be created
fi

Tapi itu benar-benar membuat file jika belum ada di sana. Kita bisa membersihkan diri kita sendiri:

if touch /path/to/file; then
    rm /path/to/file
fi

Tapi ini akan menghapus file yang sudah ada, yang mungkin tidak Anda inginkan.

Namun, kami memiliki cara untuk mengatasi hal ini:

if mkdir /path/to/file; then
    rmdir /path/to/file
fi

Anda tidak dapat memiliki direktori dengan nama yang sama dengan objek lain di direktori itu. Saya tidak dapat memikirkan situasi di mana Anda dapat membuat direktori tetapi tidak membuat file. Setelah tes ini, skrip Anda akan bebas untuk membuat yang konvensional /path/to/filedan melakukan apa pun yang diinginkan.


2
Itu sangat rapi menangani begitu banyak masalah! Komentar kode menjelaskan & membenarkan bahwa solusi yang tidak biasa mungkin melebihi panjang jawaban Anda. :-)
jpaugh

Saya juga telah digunakan if mkdir /var/run/lockdirsebagai tes penguncian. Ini adalah atomik, sementara if ! [[ -f /var/run/lockfile ]]; then touch /var/run/lockfilememiliki sedikit waktu di antara pengujian dan di touchmana contoh skrip yang lain dapat dimulai.
DopeGhoti

8

Dari apa yang saya kumpulkan, Anda ingin memeriksa itu ketika menggunakan

tee -- "$OUT_FILE"

(perhatikan --atau tidak akan berfungsi untuk nama file yang dimulai dengan -), teeakan berhasil membuka file untuk ditulis.

Yaitu:

  • panjang path file tidak melebihi batas PATH_MAX
  • file ada (setelah resolusi symlink) dan bukan tipe direktori dan Anda memiliki izin tertulis untuk itu.
  • jika file tidak ada, dirname file ada (setelah resolusi symlink) sebagai direktori dan Anda telah menulis dan mencari izin untuk itu dan panjang nama file tidak melebihi batas NAME_MAX dari sistem file tempat direktori berada.
  • atau file tersebut adalah symlink yang menunjuk ke file yang tidak ada dan bukan loop symlink tetapi memenuhi kriteria di atas

Kami akan mengabaikan untuk sekarang filesystems seperti vfat, ntfs atau hfsplus yang memiliki batasan pada apa nilai byte yang mungkin berisi nama file, kuota disk, batas proses, selinux, apparmor atau mekanisme keamanan lainnya dengan cara, filesystem lengkap, tidak ada inode tersisa, perangkat file yang tidak dapat dibuka seperti itu karena suatu alasan atau lainnya, file yang dapat dieksekusi saat ini dipetakan dalam beberapa ruang alamat proses yang semuanya juga dapat mempengaruhi kemampuan untuk membuka atau membuat file.

Dengan zsh:

zmodload zsh/system
tee_would_likely_succeed() {
  local file=$1 ERRNO=0 LC_ALL=C
  if [ -d "$file" ]; then
    return 1 # directory
  elif [ -w "$file" ]; then
    return 0 # writable non-directory
  elif [ -e "$file" ]; then
    return 1 # exists, non-writable
  elif [ "$errnos[ERRNO]" != ENOENT ]; then
    return 1 # only ENOENT error can be recovered
  else
    local dir=$file:P:h base=$file:t
    [ -d "$dir" ] &&    # directory
      [ -w "$dir" ] &&  # writable
      [ -x "$dir" ] &&  # and searchable
      (($#base <= $(getconf -- NAME_MAX "$dir")))
    return
  fi
}

Dalam bashatau shell seperti Bourne, ganti saja

zmodload zsh/system
tee_would_likely_succeed() {
  <zsh-code>
}

dengan:

tee_would_likely_succeed() {
  zsh -s -- "$@" << 'EOF'
  zmodload zsh/system
  <zsh-code>
EOF
}

Fitur- zshspesifik di sini adalah $ERRNO(yang memperlihatkan kode kesalahan dari panggilan sistem terakhir) dan $errnos[]array asosiatif untuk menerjemahkan ke nama makro C standar yang sesuai. Dan $var:h(dari csh) dan $var:P(perlu zsh 5.3 atau di atas).

bash belum memiliki fitur yang setara.

$file:hbisa diganti dengan dir=$(dirname -- "$file"; echo .); dir=${dir%??}, atau dengan GNU dirname: IFS= read -rd '' dir < <(dirname -z -- "$file").

Sebab $errnos[ERRNO] == ENOENT, pendekatan bisa dijalankan ls -Ldpada file dan memeriksa apakah pesan kesalahan sesuai dengan kesalahan ENOENT. Melakukan hal itu dengan andal dan mudah dibawa memang rumit.

Satu pendekatan dapat berupa:

msg_for_ENOENT=$(LC_ALL=C ls -d -- '/no such file' 2>&1)
msg_for_ENOENT=${msg_for_ENOENT##*:}

(dengan asumsi bahwa pesan kesalahan berakhir dengan syserror()terjemahan ENOENTdan bahwa terjemahan itu tidak menyertakan a :) dan kemudian, alih-alih [ -e "$file" ], lakukan:

err=$(ls -Ld -- "$file" 2>&1)

Dan periksa kesalahan ENOENT dengan

case $err in
  (*:"$msg_for_ENOENT") ...
esac

Bagian $file:Padalah yang paling sulit untuk dicapaibash , terutama pada FreeBSD.

FreeBSD memang memiliki realpathperintah dan readlinkperintah yang menerima -fopsi, tetapi mereka tidak dapat digunakan dalam kasus di mana file tersebut adalah symlink yang tidak menyelesaikan. Itu sama dengan perl'sCwd::realpath() .

python's os.path.realpath()tidak muncul untuk bekerja sama dengan zsh $file:P, sehingga dengan asumsi bahwa setidaknya satu versi dari pythonterinstal dan bahwa ada pythonperintah yang mengacu pada salah satu dari mereka (yang tidak diberikan pada FreeBSD), Anda bisa melakukan:

dir=$(python -c '
import os, sys
print(os.path.realpath(sys.argv[1]) + ".")' "$dir") || return
dir=${dir%.}

Tapi kemudian, Anda bisa melakukan semuanya python.

Atau Anda bisa memutuskan untuk tidak menangani semua kasus sudut itu.


Ya, Anda memahami maksud saya dengan benar. Sebenarnya, pertanyaan awal tidak jelas: Saya awalnya berbicara tentang membuat file, tetapi sebenarnya saya menggunakannya dengan teebegitu persyaratannya adalah bahwa file tersebut dapat dibuat jika tidak ada, atau dapat dipotong ke nol dan ditimpa jika ya (atau yang lain teemenangani itu).
BeeOnRope

Sepertinya hasil edit terakhir Anda menghapus format
D. Ben Knoble

Terima kasih, @ uskup, @ D.BenKnoble, lihat edit.
Stéphane Chazelas

1
@ DennisWilliamson, well ya, shell adalah alat untuk menjalankan program, dan setiap program yang Anda panggil dalam skrip Anda harus diinstal agar skrip Anda berfungsi, tidak perlu dikatakan lagi. macOS hadir dengan bash dan zsh yang terinstal secara default. FreeBSD tidak memiliki keduanya. OP mungkin punya alasan bagus untuk ingin melakukan itu di bash, mungkin itu adalah sesuatu yang ingin mereka tambahkan ke skrip bash yang sudah ditulis.
Stéphane Chazelas

1
@pipe, sekali lagi, shell adalah penerjemah perintah. Anda akan melihat bahwa banyak kutipan kode shell yang ditulis di sini Invoke interpreter bahasa lain seperti perl, awk, sedatau python(atau bahkan sh, bash...). Shell scripting adalah tentang menggunakan perintah terbaik untuk tugas tersebut. Sini bahkan perltidak memiliki setara dengan zsh's $var:P(yang Cwd::realpathberperilaku seperti BSD realpathatau readlink -fsaat Anda ingin untuk berperilaku seperti GNU readlink -fatau python' s os.path.realpath())
Stéphane Chazelas

6

Salah satu opsi yang mungkin ingin Anda pertimbangkan adalah membuat file sejak awal tetapi hanya mengisi nanti di skrip Anda. Anda dapat menggunakan execperintah untuk membuka file dalam deskriptor file (seperti 3, 4, dll.) Dan kemudian menggunakan redirection ke deskriptor file ( >&3, dll.) Untuk menulis konten ke file itu.

Sesuatu seperti:

#!/bin/bash

# Open the file for read/write, so it doesn't get
# truncated just yet (to preserve the contents in
# case the initial checks fail.)
exec 3<>dir/file.txt || {
    echo "Error creating dir/file.txt" >&2
    exit 1
}

# Long checks here...
check_ok || {
    echo "Failed checks" >&2
    # cleanup file before bailing out
    rm -f dir/file.txt
    exit 1
}

# We're ready to write, first truncate the file.
# Use "truncate(1)" from coreutils, and pass it
# /dev/fd/3 so the file doesn't need to be reopened.
truncate -s 0 /dev/fd/3

# Now populate the file, use a redirection to write
# to the previously opened file descriptor.
populate_contents >&3

Anda juga dapat menggunakan trapuntuk membersihkan file pada kesalahan, itu adalah praktik umum.

Dengan cara ini, Anda mendapatkan cek nyata untuk izin bahwa Anda akan dapat membuat file, sementara pada saat yang sama dapat melakukannya cukup awal sehingga jika gagal Anda belum menghabiskan waktu menunggu cek panjang.


DIPERBARUI: Untuk menghindari clobbering file jika cek gagal, gunakan fd<>filepengalihan bash yang tidak memotong file segera. (Kami tidak peduli tentang membaca dari file, ini hanya solusi sehingga kami tidak memotongnya. Menambahkan dengan>> mungkin hanya akan bekerja juga, tapi saya cenderung menemukan ini sedikit lebih elegan, menjaga bendera O_APPEND keluar gambar.)

Ketika kita siap untuk mengganti konten, kita perlu memotong file terlebih dahulu (jika tidak, kita menulis byte lebih sedikit daripada yang ada di file sebelumnya, trailing byte akan tetap di sana.) Kita dapat menggunakan truncate (1) perintah dari coreutils untuk tujuan itu, dan kita dapat memberikannya deskriptor file terbuka yang kita miliki (menggunakan file /dev/fd/3pseudo) sehingga tidak perlu membuka kembali file. (Sekali lagi, secara teknis sesuatu yang lebih sederhana seperti : >dir/file.txtmungkin akan berfungsi, tetapi tidak harus membuka kembali file adalah solusi yang lebih elegan.)


Saya telah mempertimbangkan hal ini, tetapi masalahnya adalah jika kesalahan tidak bersalah terjadi di suatu tempat antara saat saya membuat file dan titik beberapa waktu kemudian di mana saya akan menulis untuk itu, pengguna mungkin akan kesal untuk mengetahui bahwa file yang ditentukan telah ditimpa .
BeeOnRope

1
@BeeOnRope opsi lain yang tidak akan merusak file adalah mencoba menambahkan apa pun ke dalamnya: echo -n >>fileatau true >> file. Tentu saja, ext4 memiliki file append-only, tetapi Anda bisa hidup dengan false positive itu.
Mosvy

1
@BeeOnRope Jika Anda perlu menyimpan (a) file yang ada atau (b) file baru (lengkap), itu pertanyaan yang berbeda dari yang Anda tanyakan. Jawaban yang bagus adalah memindahkan file yang ada ke nama baru (mis. my-file.txt.backup) Sebelum membuat yang baru. Solusi lain adalah dengan menulis file baru ke file sementara di folder yang sama, dan kemudian menyalinnya di file lama setelah sisa skrip berhasil --- jika operasi terakhir gagal, pengguna dapat memperbaiki masalah secara manual tanpa kehilangan kemajuannya.
jpaugh

1
@BeeOnRope Jika Anda memilih rute "penggantian atom" (yang bagus!) Maka biasanya Anda ingin menulis file sementara di direktori yang sama dengan file terakhir (mereka harus berada di sistem file yang sama untuk mengganti nama (2) untuk berhasil) dan menggunakan nama unik (jadi jika ada dua contoh skrip berjalan, mereka tidak akan saling menghancurkan file sementara masing-masing.) Membuka file sementara untuk menulis di direktori yang sama biasanya merupakan indikasi yang baik bagi Anda Akan dapat mengganti nama nanti (well, kecuali jika target akhir ada dan merupakan direktori), jadi sampai batas tertentu ia juga menjawab pertanyaan Anda.
filbranden

1
@ jpaugh - Saya cukup enggan untuk melakukan itu. Ini pasti akan mengarah pada jawaban yang mencoba memecahkan masalah level saya yang lebih tinggi, yang mungkin mengakui solusi yang tidak ada hubungannya dengan pertanyaan "Bagaimana saya bisa memeriksa ...". Terlepas dari apa masalah tingkat tinggi saya, saya pikir pertanyaan ini berdiri sendiri sebagai sesuatu yang mungkin ingin Anda lakukan. Tidak adil bagi orang yang menjawab pertanyaan spesifik itu jika saya mengubahnya untuk "menyelesaikan masalah spesifik yang saya alami dengan skrip saya". Saya memasukkan rincian itu di sini karena saya ditanya, tetapi saya tidak ingin mengubah pertanyaan utama.
BeeOnRope

4

Saya pikir solusi DopeGhoti lebih baik tetapi ini juga harus bekerja:

file=$1
if [[ "${file:0:1}" == '/' ]]; then
    dir=${file%/*}
elif [[ "$file" =~ .*/.* ]]; then
    dir="$(PWD)/${file%/*}"
else
    dir=$(PWD)
fi

if [[ -w "$dir" ]]; then
    echo "writable"
    #do stuff with writable file
else
    echo "not writable"
    #do stuff without writable file
fi

Yang pertama jika konstruk memeriksa apakah argumennya adalah path lengkap (dimulai dengan /) dan menetapkan dirvariabel ke path direktori hingga yang terakhir /. Kalau tidak, jika argumen tidak dimulai dengan /tetapi mengandung /(menentukan sub direktori) itu akan diatur dirke direktori kerja sekarang + jalur direktori sub. Kalau tidak, itu mengasumsikan direktori kerja saat ini. Kemudian memeriksa apakah direktori itu dapat ditulisi.


4

Bagaimana dengan menggunakan testperintah normal seperti diuraikan di bawah ini?

FILE=$1

DIR=$(dirname $FILE) # $DIR now contains '.' for file names only, 'foo' for 'foo/bar'

if [ -d $DIR ] ; then
  echo "base directory $DIR for file exists"
  if [ -e $FILE ] ; then
    if [ -w $FILE ] ; then
      echo "file exists, is writeable"
    else
      echo "file exists, NOT writeable"
    fi
  elif [ -w $DIR ] ; then
    echo "directory is writeable"
  else
    echo "directory is NOT writeable"
  fi
else
  echo "can NOT create file in non-existent directory $DIR "
fi

Saya hampir menggandakan jawaban ini! Pengantar yang bagus, tetapi Anda mungkin menyebutkan man test, dan itu [ ]setara dengan tes (bukan pengetahuan umum lagi). Berikut ini adalah kalimat yang akan saya gunakan dalam jawaban saya yang lebih rendah, untuk membahas kasus yang tepat, untuk anak cucu:if [ -w $1] && [ ! -d $1 ] ; then echo "do stuff"; fi
Iron Gremlin

4

Anda menyebutkan pengalaman pengguna mengarahkan pertanyaan Anda. Saya akan menjawab dari sudut UX, karena Anda sudah mendapatkan jawaban yang baik di sisi teknis.

Daripada melakukan pemeriksaan di muka, bagaimana dengan menulis hasil ke file sementara kemudian di bagian paling akhir, menempatkan hasilnya ke file yang diinginkan pengguna? Suka:

userfile=${1:?Where would you like the file written?}
tmpfile=$(mktemp)

# ... all the complicated stuff, writing into "${tmpfile}"

# fill user's file, keeping existing permissions or creating anew
# while respecting umask
cat "${tmpfile}" > "${userfile}"
if [ 0 -eq $? ]; then
    rm "${tmpfile}"
else
    echo "Couldn't write results into ${userfile}." >&2
    echo "Results available in ${tmpfile}." >&2
    exit 1
fi

Yang baik dengan pendekatan ini: ini menghasilkan operasi yang diinginkan dalam skenario jalur bahagia normal, langkah-langkah sisi masalah atomisitas uji-dan-set, mempertahankan izin file target sambil membuat jika perlu, dan sudah mati sederhana untuk diimplementasikan.

Catatan: jika kami menggunakan mv, kami akan menjaga izin file sementara - kami tidak menginginkan itu, saya pikir: kami ingin menjaga izin seperti yang ditetapkan pada file target.

Sekarang, yang buruk: ini membutuhkan ruang dua kali lipat ( cat .. >membangun), memaksa pengguna untuk melakukan pekerjaan manual jika file target tidak dapat ditulisi pada saat diperlukan, dan meninggalkan file sementara tergeletak di sekitar (yang mungkin memiliki keamanan atau masalah pemeliharaan).


Sebenarnya, ini kurang lebih apa yang saya lakukan sekarang. Saya menulis sebagian besar hasil ke file sementara dan kemudian pada akhir melakukan langkah pemrosesan akhir dan menulis hasilnya ke file akhir. Masalahnya adalah saya ingin menebus lebih awal (di awal skrip) jika langkah terakhir itu kemungkinan gagal. Script dapat berjalan tanpa pengawasan selama beberapa menit atau jam, jadi Anda benar-benar ingin tahu di muka bahwa itu pasti gagal!
BeeOnRope

Tentu, tetapi ada banyak cara ini bisa gagal: disk bisa mengisi, direktori hulu bisa dihapus, izin bisa berubah, file target mungkin digunakan untuk beberapa hal penting lainnya dan pengguna lupa ia menetapkan file yang sama untuk dihancurkan oleh ini operasi. Jika kita membicarakan ini dari perspektif UX murni, maka mungkin hal yang benar untuk dilakukan adalah memperlakukannya seperti pengiriman pekerjaan: pada akhirnya, ketika Anda tahu itu berfungsi dengan benar sampai selesai, beri tahu pengguna di mana konten yang dihasilkan berada dan tawarkan sebuah menyarankan perintah bagi mereka untuk bergerak sendiri.
Uskup

Secara teori, ya ada cara tak terbatas yang bisa gagal. Dalam praktiknya, sebagian besar waktu gagal ini adalah karena jalur yang disediakan tidak valid. Saya tidak dapat mencegah beberapa pengguna jahat dari memodifikasi FS secara bersamaan untuk memutus pekerjaan di tengah operasi, tetapi saya pasti dapat memeriksa penyebab kegagalan # 1 dari jalur yang tidak valid atau tidak dapat ditulisi.
BeeOnRope

2

TL; DR:

: >> "${userfile}"

Tidak seperti <> "${userfile}"atau touch "${userfile}", ini tidak akan membuat modifikasi palsu pada stempel waktu file dan juga akan bekerja dengan file tulis saja.


Dari OP:

Saya ingin melakukan pemeriksaan yang masuk akal apakah file tersebut dapat dibuat / ditimpa, tetapi sebenarnya tidak dibuat.

Dan dari komentar Anda hingga jawaban saya dari perspektif UX :

Sebagian besar waktu gagal ini adalah karena jalur yang disediakan tidak valid. Saya tidak bisa mencegah beberapa pengguna jahat mengubah FS secara bersamaan untuk memutus pekerjaan di tengah operasi, tetapi saya pasti dapat memeriksa penyebab kegagalan # 1 dari jalur yang tidak valid atau tidak dapat ditulisi.

Satu-satunya tes yang dapat diandalkan adalah ke open(2)file, karena hanya itu yang menyelesaikan setiap pertanyaan tentang kemampuan menulis: jalur, kepemilikan, sistem file, jaringan, konteks keamanan, dll. Tes lain akan membahas beberapa bagian kemampuan menulis, tetapi tidak yang lain. Jika Anda ingin bagian dari tes, pada akhirnya Anda harus memilih apa yang penting bagi Anda.

Tapi ini pemikiran lain. Dari apa yang saya mengerti:

  1. proses pembuatan konten sudah berjalan lama, dan
  2. file target harus dibiarkan dalam keadaan konsisten.

Anda ingin melakukan pra-pemeriksaan ini karena # 1, dan Anda tidak ingin mengutak-atik file yang ada karena # 2. Jadi mengapa Anda tidak meminta shell untuk membuka file append, tetapi tidak benar-benar menambahkan apa pun?

$ tree -ps
.
├── [dr-x------        4096]  dir_r
├── [drwx------        4096]  dir_w
├── [-r--------           0]  file_r
└── [-rw-------           0]  file_w

$ for p in file_r dir_r/foo file_w dir_w/foo; do : >> $p; done
-bash: file_r: Permission denied
-bash: dir_r/foo: Permission denied

$ tree -ps
.
├── [dr-x------        4096]  dir_r
├── [drwx------        4096]  dir_w
   └── [-rw-rw-r--           0]  foo
├── [-r--------           0]  file_r
└── [-rw-------           0]  file_w

Di bawah tenda, ini menyelesaikan pertanyaan kemampuan menulis persis seperti yang diinginkan:

open("dir_w/foo", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3

tetapi tanpa memodifikasi isi atau metadata file. Sekarang, ya, pendekatan ini:

  • tidak memberi tahu Anda jika file hanya ditambahkan, yang mungkin menjadi masalah ketika Anda memutakhirkannya di akhir pembuatan konten Anda. Anda dapat mendeteksi ini, sampai taraf tertentu, dengan lsattrdan bereaksi sesuai.
  • membuat file yang sebelumnya tidak ada, jika demikian: mitigasi ini dengan selektif rm.

Sementara saya berpendapat (dalam jawaban saya yang lain) bahwa pendekatan yang paling ramah pengguna adalah membuat file sementara yang harus dipindahkan oleh pengguna, saya pikir ini adalah pendekatan yang paling tidak ramah pengguna untuk sepenuhnya memeriksa input mereka.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.