mirror of https://github.com/akuker/RASCSI.git
commit
57fa874d53
|
@ -93,28 +93,16 @@ jobs:
|
|||
working-directory: cpp
|
||||
run: gcov --preserve-paths $(find -name '*.gcno')
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Cache SonarCloud scan cache
|
||||
id: sonar-scan-cache
|
||||
with:
|
||||
path: ~/.sonar_cache/
|
||||
key: sonar-scan-cache-${{ env.SONAR_SCANNER_VERSION }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
restore-keys: |
|
||||
sonar-scan-cache-${{ env.SONAR_SCANNER_VERSION }}-${{ github.ref_name }}
|
||||
|
||||
- name: Run sonar-scanner
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: >-
|
||||
(mkdir -p $HOME/.sonar_cache || true) &&
|
||||
$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin/sonar-scanner
|
||||
--define sonar.host.url="${{ env.SONAR_SERVER_URL }}"
|
||||
--define sonar.projectKey=${{ env.SONAR_PROJECT_KEY }}
|
||||
--define sonar.organization=${{ env.SONAR_ORGANIZATION }}
|
||||
--define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
|
||||
--define sonar.cfamily.gcov.reportsPath=.
|
||||
--define sonar.cfamily.cache.enabled=true
|
||||
--define sonar.cfamily.cache.path="$HOME/.sonar_cache/"
|
||||
--define sonar.coverage.exclusions="cpp/**/test/**"
|
||||
--define sonar.cpd.exclusions="cpp/**/test/**"
|
||||
--define sonar.inclusions="cpp/**,python/**"
|
||||
|
|
16
cpp/Makefile
16
cpp/Makefile
|
@ -199,22 +199,22 @@ docs: $(DOC_DIR)/piscsi_man_page.txt $(DOC_DIR)/scsictl_man_page.txt $(DOC_DIR)/
|
|||
$(SRC_PISCSI_CORE) $(SRC_SCSICTL_CORE) : $(OBJ_GENERATED)
|
||||
|
||||
$(BINDIR)/$(PISCSI): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs
|
||||
|
||||
$(BINDIR)/$(SCSICTL): $(SRC_GENERATED) $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lprotobuf
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lprotobuf
|
||||
|
||||
$(BINDIR)/$(SCSIDUMP): $(OBJ_SCSIDUMP) $(OBJ_SHARED) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSIDUMP) $(OBJ_SHARED)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SCSIDUMP) $(OBJ_SHARED)
|
||||
|
||||
$(BINDIR)/$(SCSIMON): $(OBJ_SCSIMON) $(OBJ_SHARED) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSIMON) $(OBJ_SHARED)
|
||||
|
||||
$(BINDIR)/$(PISCSI_TEST): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SCSICTL_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) $(TEST_WRAPS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs -lgmock -lgtest
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SCSIMON) $(OBJ_SHARED)
|
||||
|
||||
$(BINDIR)/$(SCSILOOP): $(OBJ_SHARED) $(OBJ_SCSILOOP) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SHARED) $(OBJ_SCSILOOP)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SHARED) $(OBJ_SCSILOOP)
|
||||
|
||||
$(BINDIR)/$(PISCSI_TEST): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SCSICTL_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(TEST_WRAPS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs -lgmock -lgtest
|
||||
|
||||
# Phony rules for building individual utilities
|
||||
.PHONY: $(PISCSI) $(SCSICTL) $(SCSIDUMP) $(SCSIMON) $(PISCSI_TEST) $(SCSILOOP)
|
||||
|
|
|
@ -59,6 +59,7 @@ DeviceFactory::DeviceFactory()
|
|||
extension_mapping["hdr"] = SCRM;
|
||||
extension_mapping["mos"] = SCMO;
|
||||
extension_mapping["iso"] = SCCD;
|
||||
extension_mapping["is1"] = SCCD;
|
||||
|
||||
device_mapping["bridge"] = SCBR;
|
||||
device_mapping["daynaport"] = SCDP;
|
||||
|
@ -118,7 +119,8 @@ shared_ptr<PrimaryDevice> DeviceFactory::CreateDevice(PbDeviceType type, int lun
|
|||
break;
|
||||
|
||||
case SCCD:
|
||||
device = make_shared<SCSICD>(lun, sector_sizes.find(SCCD)->second);
|
||||
device = make_shared<SCSICD>(lun, sector_sizes.find(SCCD)->second,
|
||||
GetExtensionLowerCase(filename) == "is1" ? scsi_level::SCSI_1_CCS : scsi_level::SCSI_2);
|
||||
device->SetProduct("SCSI CD-ROM");
|
||||
break;
|
||||
|
||||
|
|
|
@ -396,7 +396,12 @@ void Disk::SetUpModePages(map<int, vector<byte>>& pages, int page, bool changeab
|
|||
void Disk::AddErrorPage(map<int, vector<byte>>& pages, bool) const
|
||||
{
|
||||
// Retry count is 0, limit time uses internal default value
|
||||
pages[1] = vector<byte>(12);
|
||||
vector<byte> buf(12);
|
||||
|
||||
// TB, PER, DTE (required for OpenVMS/VAX compatibility, see issue #1117)
|
||||
buf[2] = (byte)0x26;
|
||||
|
||||
pages[1] = buf;
|
||||
}
|
||||
|
||||
void Disk::AddFormatPage(map<int, vector<byte>>& pages, bool changeable) const
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
using namespace scsi_defs;
|
||||
using namespace scsi_command_util;
|
||||
|
||||
SCSICD::SCSICD(int lun, const unordered_set<uint32_t>& sector_sizes) : Disk(SCCD, lun)
|
||||
SCSICD::SCSICD(int lun, const unordered_set<uint32_t>& sector_sizes, scsi_defs::scsi_level level)
|
||||
: Disk(SCCD, lun), scsi_level(level)
|
||||
{
|
||||
SetSectorSizes(sector_sizes);
|
||||
|
||||
|
@ -164,7 +165,7 @@ void SCSICD::ReadToc()
|
|||
|
||||
vector<uint8_t> SCSICD::InquiryInternal() const
|
||||
{
|
||||
return HandleInquiry(device_type::CD_ROM, scsi_level::SCSI_2, true);
|
||||
return HandleInquiry(device_type::CD_ROM, scsi_level, true);
|
||||
}
|
||||
|
||||
void SCSICD::SetUpModePages(map<int, vector<byte>>& pages, int page, bool changeable) const
|
||||
|
|
|
@ -22,7 +22,7 @@ class SCSICD : public Disk, private ScsiMmcCommands
|
|||
{
|
||||
public:
|
||||
|
||||
SCSICD(int, const unordered_set<uint32_t>&);
|
||||
SCSICD(int, const unordered_set<uint32_t>&, scsi_defs::scsi_level = scsi_level::SCSI_2);
|
||||
~SCSICD() override = default;
|
||||
|
||||
bool Init(const unordered_map<string, string>&) override;
|
||||
|
@ -43,6 +43,7 @@ private:
|
|||
|
||||
void AddCDROMPage(map<int, vector<byte>>&, bool) const;
|
||||
void AddCDDAPage(map<int, vector<byte>>&, bool) const;
|
||||
scsi_defs::scsi_level scsi_level;
|
||||
|
||||
void OpenIso();
|
||||
void OpenPhysical();
|
||||
|
|
|
@ -36,9 +36,9 @@ string SCSIHD::GetProductData() const
|
|||
uint64_t capacity = GetBlockCount() * GetSectorSizeInBytes();
|
||||
string unit;
|
||||
|
||||
// 10 GiB and more
|
||||
if (capacity >= 1'099'511'627'776) {
|
||||
capacity /= 1'099'511'627'776;
|
||||
// 10,000 MiB and more
|
||||
if (capacity >= 10'485'760'000) {
|
||||
capacity /= 1'073'741'824;
|
||||
unit = "GiB";
|
||||
}
|
||||
// 1 MiB and more
|
||||
|
|
|
@ -87,7 +87,7 @@ bool GPIOBUS_Raspberry::Init(mode_e mode)
|
|||
// Map peripheral region memory
|
||||
void *map = mmap(NULL, 0x1000100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, baseaddr);
|
||||
if (map == MAP_FAILED) {
|
||||
LOGERROR("Error: Unable to map memory")
|
||||
LOGERROR("Error: Unable to map memory: %s", strerror(errno))
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
|
@ -985,4 +985,4 @@ uint32_t GPIOBUS_Raspberry::Acquire()
|
|||
#endif // SIGNAL_CONTROL_MODE
|
||||
|
||||
return signals;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,8 @@ void Piscsi::Banner(const vector<char *>& args) const
|
|||
cout << " hdi : SCSI HD image (Anex86 HD image)\n";
|
||||
cout << " nhd : SCSI HD image (T98Next HD image)\n";
|
||||
cout << " mos : SCSI MO image (MO image)\n";
|
||||
cout << " iso : SCSI CD image (ISO 9660 image)\n" << flush;
|
||||
cout << " iso : SCSI CD image (ISO 9660 image)\n";
|
||||
cout << " is1 : SCSI CD image (ISO 9660 image, SCSI-1)\n" << flush;
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
// The following should be updated for each release
|
||||
const int piscsi_major_version = 23; // Last two digits of year
|
||||
const int piscsi_minor_version = 2; // Month
|
||||
const int piscsi_minor_version = 4; // Month
|
||||
const int piscsi_patch_version = 1; // Patch number - increment for each update
|
||||
|
||||
using namespace std;
|
||||
|
|
|
@ -29,6 +29,7 @@ TEST(DeviceFactoryTest, GetTypeForFile)
|
|||
EXPECT_EQ(device_factory.GetTypeForFile("test.hdr"), SCRM);
|
||||
EXPECT_EQ(device_factory.GetTypeForFile("test.mos"), SCMO);
|
||||
EXPECT_EQ(device_factory.GetTypeForFile("test.iso"), SCCD);
|
||||
EXPECT_EQ(device_factory.GetTypeForFile("test.is1"), SCCD);
|
||||
EXPECT_EQ(device_factory.GetTypeForFile("test.suffix.iso"), SCCD);
|
||||
EXPECT_EQ(device_factory.GetTypeForFile("bridge"), SCBR);
|
||||
EXPECT_EQ(device_factory.GetTypeForFile("daynaport"), SCDP);
|
||||
|
@ -79,7 +80,7 @@ TEST(DeviceFactoryTest, GetExtensionMapping)
|
|||
DeviceFactory device_factory;
|
||||
|
||||
unordered_map<string, PbDeviceType> mapping = device_factory.GetExtensionMapping();
|
||||
EXPECT_EQ(9, mapping.size());
|
||||
EXPECT_EQ(10, mapping.size());
|
||||
EXPECT_EQ(SCHD, mapping["hd1"]);
|
||||
EXPECT_EQ(SCHD, mapping["hds"]);
|
||||
EXPECT_EQ(SCHD, mapping["hda"]);
|
||||
|
@ -89,6 +90,7 @@ TEST(DeviceFactoryTest, GetExtensionMapping)
|
|||
EXPECT_EQ(SCRM, mapping["hdr"]);
|
||||
EXPECT_EQ(SCMO, mapping["mos"]);
|
||||
EXPECT_EQ(SCCD, mapping["iso"]);
|
||||
EXPECT_EQ(SCCD, mapping["is1"]);
|
||||
}
|
||||
|
||||
TEST(DeviceFactoryTest, GetDefaultParams)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2022 Uwe Seimet
|
||||
// Copyright (C) 2022-2023 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
@ -358,6 +358,7 @@ class MockSCSIHD : public SCSIHD //NOSONAR Ignore inheritance hierarchy depth in
|
|||
class MockSCSIHD_NEC : public SCSIHD_NEC //NOSONAR Ignore inheritance hierarchy depth in unit tests
|
||||
{
|
||||
FRIEND_TEST(ScsiHdNecTest, SetUpModePages);
|
||||
FRIEND_TEST(ScsiHdNecTest, TestAddErrorPage);
|
||||
FRIEND_TEST(ScsiHdNecTest, TestAddFormatPage);
|
||||
FRIEND_TEST(ScsiHdNecTest, TestAddDrivePage);
|
||||
FRIEND_TEST(PiscsiExecutorTest, ProcessDeviceCmd);
|
||||
|
|
|
@ -39,6 +39,8 @@ TEST(PiscsiImageTest, CreateImage)
|
|||
PbCommand command;
|
||||
PiscsiImage image;
|
||||
|
||||
StorageDevice::UnreserveAll();
|
||||
|
||||
EXPECT_FALSE(image.CreateImage(context, command)) << "Filename must be reported as missing";
|
||||
|
||||
SetParam(command, "file", "/a/b/c/filename");
|
||||
|
@ -63,6 +65,8 @@ TEST(PiscsiImageTest, DeleteImage)
|
|||
PbCommand command;
|
||||
PiscsiImage image;
|
||||
|
||||
StorageDevice::UnreserveAll();
|
||||
|
||||
EXPECT_FALSE(image.DeleteImage(context, command)) << "Filename must be reported as missing";
|
||||
|
||||
SetParam(command, "file", "/a/b/c/filename");
|
||||
|
@ -82,6 +86,8 @@ TEST(PiscsiImageTest, RenameImage)
|
|||
PbCommand command;
|
||||
PiscsiImage image;
|
||||
|
||||
StorageDevice::UnreserveAll();
|
||||
|
||||
EXPECT_FALSE(image.RenameImage(context, command)) << "Source filename must be reported as missing";
|
||||
|
||||
SetParam(command, "from", "/a/b/c/filename_from");
|
||||
|
@ -99,6 +105,8 @@ TEST(PiscsiImageTest, CopyImage)
|
|||
PbCommand command;
|
||||
PiscsiImage image;
|
||||
|
||||
StorageDevice::UnreserveAll();
|
||||
|
||||
EXPECT_FALSE(image.CopyImage(context, command)) << "Source filename must be reported as missing";
|
||||
|
||||
SetParam(command, "from", "/a/b/c/filename_from");
|
||||
|
@ -116,6 +124,8 @@ TEST(PiscsiImageTest, SetImagePermissions)
|
|||
PbCommand command;
|
||||
PiscsiImage image;
|
||||
|
||||
StorageDevice::UnreserveAll();
|
||||
|
||||
EXPECT_FALSE(image.SetImagePermissions(context, command)) << "Filename must be reported as missing";
|
||||
|
||||
SetParam(command, "file", "/a/b/c/filename");
|
||||
|
|
|
@ -221,5 +221,5 @@ TEST(PiscsiResponseTest, GetMappingInfo)
|
|||
|
||||
const auto& info = response.GetMappingInfo(result);
|
||||
EXPECT_TRUE(result.status());
|
||||
EXPECT_EQ(9, info->mapping().size());
|
||||
EXPECT_EQ(10, info->mapping().size());
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ void ScsiCdTest_SetUpModePages(map<int, vector<byte>>& pages)
|
|||
TEST(ScsiCdTest, Inquiry)
|
||||
{
|
||||
TestInquiry(SCCD, device_type::CD_ROM, scsi_level::SCSI_2, "PiSCSI SCSI CD-ROM ", 0x1f, true);
|
||||
|
||||
TestInquiry(SCCD, device_type::CD_ROM, scsi_level::SCSI_1_CCS, "PiSCSI SCSI CD-ROM ", 0x1f, true, ".is1");
|
||||
}
|
||||
|
||||
TEST(ScsiCdTest, SetUpModePages)
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// SCSI Target Emulator PiSCSI
|
||||
// for Raspberry Pi
|
||||
//
|
||||
// Copyright (C) 2022 Uwe Seimet
|
||||
// Copyright (C) 2022-2023 Uwe Seimet
|
||||
//
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
|
@ -47,6 +47,20 @@ TEST(ScsiHdNecTest, SetUpModePages)
|
|||
ScsiHdNecTest_SetUpModePages(pages);
|
||||
}
|
||||
|
||||
TEST(ScsiHdNecTest, TestAddErrorPage)
|
||||
{
|
||||
map<int, vector<byte>> pages;
|
||||
MockSCSIHD_NEC hd(0);
|
||||
|
||||
hd.SetBlockCount(0x1234);
|
||||
hd.SetReady(true);
|
||||
// Non changeable
|
||||
hd.SetUpModePages(pages, 0x01, false);
|
||||
EXPECT_EQ(1, pages.size()) << "Unexpected number of mode pages";
|
||||
const vector<byte>& page_1 = pages[1];
|
||||
EXPECT_EQ(0x26, to_integer<int>(page_1[2]));
|
||||
}
|
||||
|
||||
TEST(ScsiHdNecTest, TestAddFormatPage)
|
||||
{
|
||||
map<int, vector<byte>> pages;
|
||||
|
|
|
@ -69,10 +69,10 @@ TEST(ScsiHdTest, GetProductData)
|
|||
|
||||
hd_gb.SetFilename(string(filename));
|
||||
hd_gb.SetSectorSizeInBytes(1024);
|
||||
hd_gb.SetBlockCount(1'099'511'627'776 / 1024);
|
||||
hd_gb.SetBlockCount(10'737'418'240 / 1024);
|
||||
hd_gb.FinalizeSetup(0);
|
||||
s = hd_gb.GetProduct();
|
||||
EXPECT_NE(string::npos, s.find("1 GiB"));
|
||||
EXPECT_NE(string::npos, s.find("10 GiB"));
|
||||
remove(filename);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ PiSCSI will determine the type of device based upon the file extension of the FI
|
|||
hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers)
|
||||
mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.)
|
||||
iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image)
|
||||
is1: SCSI CD-ROM or DVD-ROM image (ISO 9660 image, SCSI-1)
|
||||
|
||||
For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command:
|
||||
sudo piscsi -ID0 /path/to/drive/hdimage.hda
|
||||
|
|
|
@ -29,6 +29,7 @@ DESCRIPTION
|
|||
hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers)
|
||||
mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.)
|
||||
iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image)
|
||||
is1: SCSI CD-ROM or DVD-ROM image (ISO 9660 image, SCSI-1)
|
||||
|
||||
For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command:
|
||||
sudo piscsi -ID0 /path/to/drive/hdimage.hda
|
||||
|
|
114
easyinstall.sh
114
easyinstall.sh
|
@ -49,8 +49,7 @@ echo -e $logo
|
|||
CONNECT_TYPE="FULLSPEC"
|
||||
# clang v11 is the latest distributed by Buster
|
||||
COMPILER="clang++-11"
|
||||
# Takes half of the CPU cores available, to avoid running out of memory on low spec devices
|
||||
CORES=$(awk 'BEGIN { x = '$(nproc)'; y = 2; print (x / y) }' | numfmt --round=up --format=%.0f)
|
||||
CORES=1
|
||||
USER=$(whoami)
|
||||
BASE=$(dirname "$(readlink -f "${0}")")
|
||||
CPP_PATH="$BASE/cpp"
|
||||
|
@ -87,6 +86,12 @@ function initialChecks() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Only to be used for pi-gen automated install
|
||||
function cacheSudo() {
|
||||
echo "Caching sudo password"
|
||||
echo raspberry | sudo -v -S
|
||||
}
|
||||
|
||||
# checks that the current user has sudoers privileges
|
||||
function sudoCheck() {
|
||||
if [[ $HEADLESS ]]; then
|
||||
|
@ -705,10 +710,34 @@ function setupWirelessNetworking() {
|
|||
sudo reboot
|
||||
}
|
||||
|
||||
# Detects or creates the file sharing directory
|
||||
function createFileSharingDir() {
|
||||
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
|
||||
echo
|
||||
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
|
||||
echo
|
||||
echo "Do you want to proceed with the installation? [y/N]"
|
||||
read -r REPLY
|
||||
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
|
||||
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
elif [ -d "$FILE_SHARE_PATH" ]; then
|
||||
echo "Found a $FILE_SHARE_PATH directory; will use it for file sharing."
|
||||
else
|
||||
echo "Creating the $FILE_SHARE_PATH directory and granting read/write permissions to all users..."
|
||||
sudo mkdir -p "$FILE_SHARE_PATH"
|
||||
sudo chown -R "$USER:$USER" "$FILE_SHARE_PATH"
|
||||
chmod -Rv 775 "$FILE_SHARE_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
# Downloads, compiles, and installs Netatalk (AppleShare server)
|
||||
function installNetatalk() {
|
||||
NETATALK_VERSION="2-230201"
|
||||
NETATALK_VERSION="230302"
|
||||
NETATALK_CONFIG_PATH="/etc/netatalk"
|
||||
NETATALK_OPTIONS="--cores=$CORES --share-name='$FILE_SHARE_NAME' --share-path='$FILE_SHARE_PATH'"
|
||||
|
||||
if [ -d "$NETATALK_CONFIG_PATH" ]; then
|
||||
echo
|
||||
|
@ -723,27 +752,26 @@ function installNetatalk() {
|
|||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
|
||||
echo
|
||||
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
|
||||
echo
|
||||
echo "Do you want to proceed with the installation? [y/N]"
|
||||
read -r REPLY
|
||||
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
|
||||
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
echo
|
||||
echo "Downloading tarball to $HOME..."
|
||||
cd $HOME || exit 1
|
||||
wget -O "netatalk-2.$NETATALK_VERSION.tar.gz" "https://github.com/rdmark/netatalk-2.x/releases/download/netatalk-2-$NETATALK_VERSION/netatalk-2.$NETATALK_VERSION.tar.gz" </dev/null
|
||||
|
||||
echo "Unpacking tarball..."
|
||||
tar -xzf "netatalk-2.$NETATALK_VERSION.tar.gz"
|
||||
rm "netatalk-2.$NETATALK_VERSION.tar.gz"
|
||||
|
||||
if [ -f "/etc/network/interfaces.d/piscsi_bridge" ]; then
|
||||
echo "PiSCSI network bridge detected. Using 'piscsi_bridge' interface for AppleTalk."
|
||||
NETATALK_OPTIONS="$NETATALK_OPTIONS --appletalk-interface=piscsi_bridge"
|
||||
fi
|
||||
|
||||
echo "Downloading netatalk-$NETATALK_VERSION to $HOME"
|
||||
cd $HOME || exit 1
|
||||
wget -O "netatalk-$NETATALK_VERSION.tar.gz" "https://github.com/rdmark/Netatalk-2.x/archive/refs/tags/netatalk-$NETATALK_VERSION.tar.gz" </dev/null
|
||||
tar -xzvf "netatalk-$NETATALK_VERSION.tar.gz"
|
||||
rm "netatalk-$NETATALK_VERSION.tar.gz"
|
||||
[[ $HEADLESS ]] && NETATALK_OPTIONS="$NETATALK_OPTIONS --headless"
|
||||
[[ $SKIP_PACKAGES ]] && NETATALK_OPTIONS="$NETATALK_OPTIONS --no-packages"
|
||||
[[ $SKIP_MAKE_CLEAN ]] && NETATALK_OPTIONS="$NETATALK_OPTIONS --no-make-clean"
|
||||
|
||||
cd "$HOME/Netatalk-2.x-netatalk-$NETATALK_VERSION/contrib/shell_utils" || exit 1
|
||||
./debian_install.sh -j="$CORES" -n="$FILE_SHARE_NAME" -p="$FILE_SHARE_PATH" || exit 1
|
||||
cd "$HOME/netatalk-2.$NETATALK_VERSION/contrib/shell_utils" || exit 1
|
||||
bash -c "./debian_install.sh $NETATALK_OPTIONS" || exit 1
|
||||
}
|
||||
|
||||
# Appends the images dir as a shared Netatalk volume
|
||||
|
@ -856,26 +884,6 @@ function installSamba() {
|
|||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
|
||||
echo
|
||||
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
|
||||
echo
|
||||
echo "Do you want to proceed with the installation? [y/N]"
|
||||
read -r REPLY
|
||||
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
|
||||
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
elif [ -d "$FILE_SHARE_PATH" ]; then
|
||||
echo "Found a $FILE_SHARE_PATH directory; will use it for file sharing."
|
||||
else
|
||||
echo "Creating the $FILE_SHARE_PATH directory and granting read/write permissions to all users..."
|
||||
sudo mkdir -p "$FILE_SHARE_PATH"
|
||||
sudo chown -R "$USER:$USER" "$FILE_SHARE_PATH"
|
||||
chmod -Rv 775 "$FILE_SHARE_PATH"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Installing dependencies..."
|
||||
sudo apt-get update || true
|
||||
|
@ -1261,6 +1269,7 @@ function runChoice() {
|
|||
;;
|
||||
7)
|
||||
echo "Installing AppleShare File Server"
|
||||
createFileSharingDir
|
||||
installNetatalk
|
||||
echo "Installing AppleShare File Server - Complete!"
|
||||
;;
|
||||
|
@ -1272,6 +1281,7 @@ function runChoice() {
|
|||
echo "WARNING: The FTP server may transfer unencrypted data over the network."
|
||||
echo "Proceed with this installation only if you are on a private, secure network."
|
||||
sudoCheck
|
||||
createFileSharingDir
|
||||
installFtp
|
||||
echo "Installing FTP File Server - Complete!"
|
||||
;;
|
||||
|
@ -1283,6 +1293,7 @@ function runChoice() {
|
|||
echo " - Create a directory in the current user's home directory where shared files will be stored"
|
||||
echo " - Create a Samba user for the current user"
|
||||
sudoCheck
|
||||
createFileSharingDir
|
||||
installSamba
|
||||
echo "Installing SMB File Server - Complete!"
|
||||
;;
|
||||
|
@ -1359,6 +1370,25 @@ function runChoice() {
|
|||
installPackagesStandalone
|
||||
compilePiscsi
|
||||
;;
|
||||
99)
|
||||
echo "Hidden setup mode for running the pi-gen utility"
|
||||
echo "This shouldn't be used by normal users"
|
||||
sudoCache
|
||||
createImagesDir
|
||||
createCfgDir
|
||||
updatePiscsiGit
|
||||
installPackages
|
||||
installHfdisk
|
||||
fetchHardDiskDrivers
|
||||
compilePiscsi
|
||||
installPiscsi
|
||||
enablePiscsiService
|
||||
preparePythonCommon
|
||||
cachePipPackages
|
||||
installPiscsiWebInterface
|
||||
installWebInterfaceService
|
||||
echo "Automated install of the PiSCSI Service $(CONNECT_TYPE) complete!"
|
||||
;;
|
||||
-h|--help|h|help)
|
||||
showMenu
|
||||
;;
|
||||
|
@ -1372,7 +1402,7 @@ function runChoice() {
|
|||
function readChoice() {
|
||||
choice=-1
|
||||
|
||||
until [ $choice -ge "0" ] && [ $choice -le "16" ]; do
|
||||
until [ $choice -ge "0" ] && ([ $choice -eq "99" ] || [ $choice -le "16" ]) ; do
|
||||
echo -n "Enter your choice (0-16) or CTRL-C to exit: "
|
||||
read -r choice
|
||||
done
|
||||
|
|
|
@ -4,7 +4,7 @@ Module for methods reading from and writing to the file system
|
|||
|
||||
import logging
|
||||
import asyncio
|
||||
from os import walk
|
||||
from os import walk, path
|
||||
from functools import lru_cache
|
||||
from pathlib import PurePath, Path
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
|
@ -57,7 +57,7 @@ class FileCmds:
|
|||
# noinspection PyMethodMayBeStatic
|
||||
def list_config_files(self):
|
||||
"""
|
||||
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
|
||||
Finds files with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
|
||||
Returns a (list) of (str) files_list
|
||||
"""
|
||||
files_list = []
|
||||
|
@ -67,6 +67,26 @@ class FileCmds:
|
|||
files_list.append(file)
|
||||
return files_list
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def list_subdirs(self, directory):
|
||||
"""
|
||||
Finds subdirs within the (str) directory dir.
|
||||
Returns a (list) of (str) subdir_list.
|
||||
"""
|
||||
subdir_list = []
|
||||
# Filter out file sharing meta data dirs
|
||||
excluded_dirs = ("Network Trash Folder", "Temporary Items", "TheVolumeSettingsFolder")
|
||||
for root, dirs, _files in walk(directory, topdown=True):
|
||||
# Strip out dirs that begin with .
|
||||
dirs[:] = [d for d in dirs if not d[0] == "."]
|
||||
for dir in dirs:
|
||||
if dir not in excluded_dirs:
|
||||
dirpath = path.join(root, dir)
|
||||
subdir_list.append(dirpath.replace(directory, "", 1))
|
||||
|
||||
subdir_list.sort()
|
||||
return subdir_list
|
||||
|
||||
def list_images(self):
|
||||
"""
|
||||
Sends a IMAGE_FILES_INFO command to the server
|
||||
|
@ -146,7 +166,15 @@ class FileCmds:
|
|||
parameters = {"file_path": file_path}
|
||||
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
try:
|
||||
file_path.unlink()
|
||||
except OSError as error:
|
||||
logging.error(error)
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.DELETEFILE_UNABLE_TO_DELETE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.DELETEFILE_SUCCESS,
|
||||
|
@ -159,18 +187,28 @@ class FileCmds:
|
|||
}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def rename_file(self, file_path, target_path):
|
||||
def rename_file(self, file_path, target_path, overwrite_target=False):
|
||||
"""
|
||||
Takes:
|
||||
- (Path) file_path for the file to rename
|
||||
- (Path) target_path for the name to rename
|
||||
- optional (bool) overwrite_target
|
||||
Returns (dict) with (bool) status, (str) msg, (dict) parameters
|
||||
"""
|
||||
parameters = {"target_path": target_path}
|
||||
if not target_path.parent.exists():
|
||||
target_path.parent.mkdir(parents=True)
|
||||
if target_path.parent.exists() and not target_path.exists():
|
||||
file_path.rename(target_path)
|
||||
|
||||
if overwrite_target or not target_path.exists():
|
||||
try:
|
||||
file_path.rename(target_path)
|
||||
except OSError as error:
|
||||
logging.error(error)
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.RENAMEFILE_SUCCESS,
|
||||
|
@ -178,23 +216,33 @@ class FileCmds:
|
|||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def copy_file(self, file_path, target_path):
|
||||
def copy_file(self, file_path, target_path, overwrite_target=False):
|
||||
"""
|
||||
Takes:
|
||||
- (Path) file_path for the file to copy from
|
||||
- (Path) target_path for the name to copy to
|
||||
- optional (bool) overwrite_target
|
||||
Returns (dict) with (bool) status, (str) msg, (dict) parameters
|
||||
"""
|
||||
parameters = {"target_path": target_path}
|
||||
if not target_path.parent.exists():
|
||||
target_path.parent.mkdir(parents=True)
|
||||
if target_path.parent.exists() and not target_path.exists():
|
||||
copyfile(str(file_path), str(target_path))
|
||||
|
||||
if overwrite_target or not target_path.exists():
|
||||
try:
|
||||
copyfile(str(file_path), str(target_path))
|
||||
except OSError as error:
|
||||
logging.error(error)
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
return {
|
||||
"status": True,
|
||||
"return_code": ReturnCodes.WRITEFILE_SUCCESS,
|
||||
|
@ -202,32 +250,41 @@ class FileCmds:
|
|||
}
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
def create_empty_image(self, target_path, size):
|
||||
def create_empty_image(self, target_path, size, overwrite_target=False):
|
||||
"""
|
||||
Takes (Path) target_path and (int) size in bytes
|
||||
Creates a new empty binary file to use as image
|
||||
Creates a new empty binary file to use as image.
|
||||
Takes:
|
||||
- (Path) target_path
|
||||
- (int) size in bytes
|
||||
- optional (bool) overwrite_target
|
||||
Returns (dict) with (bool) status, (str) msg, (dict) parameters
|
||||
"""
|
||||
parameters = {"target_path": target_path}
|
||||
if not target_path.parent.exists():
|
||||
target_path.parent.mkdir(parents=True)
|
||||
if target_path.parent.exists() and not target_path.exists():
|
||||
|
||||
if overwrite_target or not target_path.exists():
|
||||
try:
|
||||
with open(f"{target_path}", "wb") as out:
|
||||
out.seek(size - 1)
|
||||
out.write(b"\0")
|
||||
except OSError as error:
|
||||
return {"status": False, "msg": str(error)}
|
||||
logging.error(error)
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
return {"status": True, "msg": ""}
|
||||
|
||||
return {
|
||||
"status": False,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
|
||||
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE,
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
|
@ -262,6 +319,7 @@ class FileCmds:
|
|||
if self.rename_file(
|
||||
Path(file["absolute_path"]),
|
||||
prop_path,
|
||||
overwrite_target=True,
|
||||
):
|
||||
properties_files_moved.append(
|
||||
{
|
||||
|
@ -317,56 +375,39 @@ class FileCmds:
|
|||
server_info = self.piscsi.get_server_info()
|
||||
full_file_path = Path(server_info["image_dir"]) / file_name
|
||||
|
||||
# Inject hfdisk commands to create Drive with correct partitions
|
||||
# https://www.codesrc.com/mediawiki/index.php/HFSFromScratch
|
||||
# i initialize partition map
|
||||
# continue with default first block
|
||||
# C Create 1st partition with type specified next)
|
||||
# continue with default
|
||||
# 32 32 blocks (required for HFS+)
|
||||
# Driver_Partition Partition Name
|
||||
# Apple_Driver Partition Type (available types: Apple_Driver,
|
||||
# Apple_Driver43, Apple_Free, Apple_HFS...)
|
||||
# C Create 2nd partition with type specified next
|
||||
# continue with default first block
|
||||
# continue with default block size (rest of the disk)
|
||||
# ${volumeName} Partition name provided by user
|
||||
# Apple_HFS Partition Type
|
||||
# w Write partition map to disk
|
||||
# y Confirm partition table
|
||||
# p Print partition map
|
||||
# Inject hfdisk commands to create Mac partition table with HFS partitions
|
||||
if disk_format == "HFS":
|
||||
partitioning_tool = "hfdisk"
|
||||
commands = [
|
||||
"i",
|
||||
"",
|
||||
"C",
|
||||
"",
|
||||
"32",
|
||||
"Driver_Partition",
|
||||
"Apple_Driver",
|
||||
"C",
|
||||
"",
|
||||
"",
|
||||
volume_name,
|
||||
"Apple_HFS",
|
||||
"w",
|
||||
"y",
|
||||
"p",
|
||||
"i", # Initialize partition map
|
||||
"", # Continue with default first block
|
||||
"C", # Create 1st partition with type specified next)
|
||||
"", # Continue with default
|
||||
"32", # 32 block (required for HFS+)
|
||||
"Driver_Partition", # Partition Name
|
||||
"Apple_Driver", # Partition Type
|
||||
"C", # Create 2nd partition with type specified next
|
||||
"", # Continue with default first block
|
||||
"", # Continue with default block size (rest of the disk)
|
||||
volume_name, # Partition name
|
||||
"Apple_HFS", # Partition Type
|
||||
"w", # Write partition map to disk
|
||||
"y", # Confirm partition table
|
||||
"p", # Print partition map (for the log)
|
||||
]
|
||||
# Create a DOS label, primary partition, W95 FAT type
|
||||
# Inject fdisk commands to create primary FAT partition with MS-DOS label
|
||||
elif disk_format == "FAT":
|
||||
partitioning_tool = "fdisk"
|
||||
commands = [
|
||||
"o",
|
||||
"n",
|
||||
"p",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"t",
|
||||
"b",
|
||||
"w",
|
||||
"o", # create a new empty DOS partition table
|
||||
"n", # add a new partition
|
||||
"p", # primary partition
|
||||
"", # default partition number
|
||||
"", # default first sector
|
||||
"", # default last sector
|
||||
"t", # change partition type
|
||||
"b", # choose W95 FAT32 type
|
||||
"w", # write table to disk and exit
|
||||
]
|
||||
try:
|
||||
process = Popen(
|
||||
|
|
|
@ -9,12 +9,14 @@ class ReturnCodes:
|
|||
|
||||
DELETEFILE_SUCCESS = 0
|
||||
DELETEFILE_FILE_NOT_FOUND = 1
|
||||
DELETEFILE_UNABLE_TO_DELETE = 2
|
||||
RENAMEFILE_SUCCESS = 10
|
||||
RENAMEFILE_UNABLE_TO_MOVE = 11
|
||||
DOWNLOADFILETOISO_SUCCESS = 20
|
||||
DOWNLOADTODIR_SUCCESS = 30
|
||||
WRITEFILE_SUCCESS = 40
|
||||
WRITEFILE_COULD_NOT_WRITE = 41
|
||||
WRITEFILE_COULD_NOT_OVERWRITE = 42
|
||||
READCONFIG_SUCCESS = 50
|
||||
READCONFIG_COULD_NOT_READ = 51
|
||||
READCONFIG_INVALID_CONFIG_FILE_FORMAT = 52
|
||||
|
|
|
@ -21,23 +21,8 @@ class SysCmds:
|
|||
@staticmethod
|
||||
def running_env():
|
||||
"""
|
||||
Returns (str) git and (str) env
|
||||
git contains the git hash of the checked out code
|
||||
env is the various system information where this app is running
|
||||
Returns (str) env, with details on the system hardware and software
|
||||
"""
|
||||
try:
|
||||
ra_git_version = (
|
||||
subprocess.run(
|
||||
["git", "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
.stdout.decode("utf-8")
|
||||
.strip()
|
||||
)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logging.warning(SHELL_ERROR, error.cmd, error.stderr.decode("utf-8"))
|
||||
ra_git_version = ""
|
||||
|
||||
PROC_MODEL_PATH = "/proc/device-tree/model"
|
||||
SYS_VENDOR_PATH = "/sys/devices/virtual/dmi/id/sys_vendor"
|
||||
|
@ -67,7 +52,6 @@ class SysCmds:
|
|||
|
||||
env = uname()
|
||||
return {
|
||||
"git": ra_git_version,
|
||||
"env": f"{hardware}, {env.system} {env.release} {env.machine}",
|
||||
}
|
||||
|
||||
|
|
|
@ -1,380 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# Copyright (c) 2021, akuker
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import RPi.GPIO as gpio
|
||||
import time
|
||||
|
||||
pin_settle_delay = 0.01
|
||||
|
||||
err_count = 0
|
||||
|
||||
# Define constants for each of the SCSI signals, based upon their
|
||||
# raspberry pi pin number (since we're using BOARD mode of RPi.GPIO)
|
||||
scsi_d0_gpio = 19
|
||||
scsi_d1_gpio = 23
|
||||
scsi_d2_gpio = 32
|
||||
scsi_d3_gpio = 33
|
||||
scsi_d4_gpio = 8
|
||||
scsi_d5_gpio = 10
|
||||
scsi_d6_gpio = 36
|
||||
scsi_d7_gpio = 11
|
||||
scsi_dp_gpio = 12
|
||||
scsi_atn_gpio = 35
|
||||
scsi_rst_gpio = 38
|
||||
scsi_ack_gpio = 40
|
||||
scsi_req_gpio = 15
|
||||
scsi_msg_gpio = 16
|
||||
scsi_cd_gpio = 18
|
||||
scsi_io_gpio = 22
|
||||
scsi_bsy_gpio = 37
|
||||
scsi_sel_gpio = 13
|
||||
|
||||
# Pin numbers of the direction controllers of the PiSCSI board
|
||||
piscsi_ind_gpio = 31
|
||||
piscsi_tad_gpio = 26
|
||||
piscsi_dtd_gpio = 24
|
||||
piscsi_none = -1
|
||||
|
||||
# Matrix showing all of the SCSI signals, along what signal they're looped back to.
|
||||
# dir_ctrl indicates which direction control pin is associated with that output
|
||||
gpio_map = [
|
||||
{
|
||||
"gpio_num": scsi_d0_gpio,
|
||||
"attached_to": scsi_ack_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d1_gpio,
|
||||
"attached_to": scsi_sel_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d2_gpio,
|
||||
"attached_to": scsi_atn_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d3_gpio,
|
||||
"attached_to": scsi_rst_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d4_gpio,
|
||||
"attached_to": scsi_cd_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d5_gpio,
|
||||
"attached_to": scsi_io_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d6_gpio,
|
||||
"attached_to": scsi_msg_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_d7_gpio,
|
||||
"attached_to": scsi_req_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_dp_gpio,
|
||||
"attached_to": scsi_bsy_gpio,
|
||||
"dir_ctrl": piscsi_dtd_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_atn_gpio,
|
||||
"attached_to": scsi_d2_gpio,
|
||||
"dir_ctrl": piscsi_ind_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_rst_gpio,
|
||||
"attached_to": scsi_d3_gpio,
|
||||
"dir_ctrl": piscsi_ind_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_ack_gpio,
|
||||
"attached_to": scsi_d0_gpio,
|
||||
"dir_ctrl": piscsi_ind_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_req_gpio,
|
||||
"attached_to": scsi_d7_gpio,
|
||||
"dir_ctrl": piscsi_tad_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_msg_gpio,
|
||||
"attached_to": scsi_d6_gpio,
|
||||
"dir_ctrl": piscsi_tad_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_cd_gpio,
|
||||
"attached_to": scsi_d4_gpio,
|
||||
"dir_ctrl": piscsi_tad_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_io_gpio,
|
||||
"attached_to": scsi_d5_gpio,
|
||||
"dir_ctrl": piscsi_tad_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_bsy_gpio,
|
||||
"attached_to": scsi_dp_gpio,
|
||||
"dir_ctrl": piscsi_tad_gpio,
|
||||
},
|
||||
{
|
||||
"gpio_num": scsi_sel_gpio,
|
||||
"attached_to": scsi_d1_gpio,
|
||||
"dir_ctrl": piscsi_ind_gpio,
|
||||
},
|
||||
]
|
||||
|
||||
# List of all of the SCSI signals that is also a dictionary to their human readable name
|
||||
scsi_signals = {
|
||||
scsi_d0_gpio: "D0",
|
||||
scsi_d1_gpio: "D1",
|
||||
scsi_d2_gpio: "D2",
|
||||
scsi_d3_gpio: "D3",
|
||||
scsi_d4_gpio: "D4",
|
||||
scsi_d5_gpio: "D5",
|
||||
scsi_d6_gpio: "D6",
|
||||
scsi_d7_gpio: "D7",
|
||||
scsi_dp_gpio: "DP",
|
||||
scsi_atn_gpio: "ATN",
|
||||
scsi_rst_gpio: "RST",
|
||||
scsi_ack_gpio: "ACK",
|
||||
scsi_req_gpio: "REQ",
|
||||
scsi_msg_gpio: "MSG",
|
||||
scsi_cd_gpio: "CD",
|
||||
scsi_io_gpio: "IO",
|
||||
scsi_bsy_gpio: "BSY",
|
||||
scsi_sel_gpio: "SEL",
|
||||
}
|
||||
|
||||
|
||||
# Debug function that just dumps the status of all of the scsi signals to the console
|
||||
def print_all():
|
||||
for cur_gpio in gpio_map:
|
||||
print(
|
||||
cur_gpio["name"] + "=" + str(gpio.input(cur_gpio["gpio_num"])) + " ",
|
||||
end="",
|
||||
flush=True,
|
||||
)
|
||||
print("")
|
||||
|
||||
|
||||
# Set transceivers IC1 and IC2 to OUTPUT
|
||||
def set_dtd_out():
|
||||
gpio.output(piscsi_dtd_gpio, gpio.LOW)
|
||||
|
||||
|
||||
# Set transceivers IC1 and IC2 to INPUT
|
||||
def set_dtd_in():
|
||||
gpio.output(piscsi_dtd_gpio, gpio.HIGH)
|
||||
|
||||
|
||||
# Set transceiver IC4 to OUTPUT
|
||||
def set_ind_out():
|
||||
gpio.output(piscsi_ind_gpio, gpio.HIGH)
|
||||
|
||||
|
||||
# Set transceiver IC4 to INPUT
|
||||
def set_ind_in():
|
||||
gpio.output(piscsi_ind_gpio, gpio.LOW)
|
||||
|
||||
|
||||
# Set transceiver IC3 to OUTPUT
|
||||
def set_tad_out():
|
||||
gpio.output(piscsi_tad_gpio, gpio.HIGH)
|
||||
|
||||
|
||||
# Set transceiver IC3 to INPUT
|
||||
def set_tad_in():
|
||||
gpio.output(piscsi_tad_gpio, gpio.LOW)
|
||||
|
||||
|
||||
# Set the specified transciever to an OUTPUT. All of the other transceivers
|
||||
# will be set to inputs. If a non-existent direction gpio is specified, this
|
||||
# will set all of the transceivers to inputs.
|
||||
def set_output_channel(out_gpio):
|
||||
if out_gpio == piscsi_tad_gpio:
|
||||
set_tad_out()
|
||||
else:
|
||||
set_tad_in()
|
||||
if out_gpio == piscsi_dtd_gpio:
|
||||
set_dtd_out()
|
||||
else:
|
||||
set_dtd_in()
|
||||
if out_gpio == piscsi_ind_gpio:
|
||||
set_ind_out()
|
||||
else:
|
||||
set_ind_in()
|
||||
|
||||
|
||||
# Main test procedure. This will execute for each of the SCSI pins to make sure its connected
|
||||
# properly.
|
||||
def test_gpio_pin(gpio_rec):
|
||||
global err_count
|
||||
|
||||
set_output_channel(gpio_rec["dir_ctrl"])
|
||||
|
||||
############################################
|
||||
# set the test gpio low
|
||||
gpio.output(gpio_rec["gpio_num"], gpio.LOW)
|
||||
|
||||
time.sleep(pin_settle_delay)
|
||||
|
||||
# loop through all of the gpios
|
||||
for cur_gpio in scsi_signals:
|
||||
# all of the gpios should be high except for the test gpio and the connected gpio
|
||||
cur_val = gpio.input(cur_gpio)
|
||||
if cur_gpio == gpio_rec["gpio_num"]:
|
||||
if cur_val != gpio.LOW:
|
||||
print(
|
||||
"Error: Test commanded GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " to be low, but it did not respond"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
elif cur_gpio == gpio_rec["attached_to"]:
|
||||
if cur_val != gpio.LOW:
|
||||
print(
|
||||
"Error: GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " should drive "
|
||||
+ scsi_signals[gpio_rec["attached_to"]]
|
||||
+ " low, but did not"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
else:
|
||||
if cur_val != gpio.HIGH:
|
||||
print(
|
||||
"Error: GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " incorrectly pulled "
|
||||
+ scsi_signals[cur_gpio]
|
||||
+ " LOW, when it shouldn't have"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
|
||||
############################################
|
||||
# set the transceivers to input
|
||||
set_output_channel(piscsi_none)
|
||||
|
||||
time.sleep(pin_settle_delay)
|
||||
|
||||
# loop through all of the gpios
|
||||
for cur_gpio in scsi_signals:
|
||||
# all of the gpios should be high except for the test gpio
|
||||
cur_val = gpio.input(cur_gpio)
|
||||
if cur_gpio == gpio_rec["gpio_num"]:
|
||||
if cur_val != gpio.LOW:
|
||||
print(
|
||||
"Error: Test commanded GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " to be low, but it did not respond"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
else:
|
||||
if cur_val != gpio.HIGH:
|
||||
print(
|
||||
"Error: GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " incorrectly pulled "
|
||||
+ scsi_signals[cur_gpio]
|
||||
+ " LOW, when it shouldn't have"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
|
||||
# Set the transceiver back to output
|
||||
set_output_channel(gpio_rec["dir_ctrl"])
|
||||
|
||||
#############################################
|
||||
# set the test gpio high
|
||||
gpio.output(gpio_rec["gpio_num"], gpio.HIGH)
|
||||
|
||||
time.sleep(pin_settle_delay)
|
||||
|
||||
# loop through all of the gpios
|
||||
for cur_gpio in scsi_signals:
|
||||
# all of the gpios should be high
|
||||
cur_val = gpio.input(cur_gpio)
|
||||
if cur_gpio == gpio_rec["gpio_num"]:
|
||||
if cur_val != gpio.HIGH:
|
||||
print(
|
||||
"Error: Test commanded GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " to be high, but it did not respond"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
else:
|
||||
if cur_val != gpio.HIGH:
|
||||
print(
|
||||
"Error: GPIO "
|
||||
+ scsi_signals[gpio_rec["gpio_num"]]
|
||||
+ " incorrectly pulled "
|
||||
+ scsi_signals[cur_gpio]
|
||||
+ " LOW, when it shouldn't have"
|
||||
)
|
||||
err_count = err_count + 1
|
||||
|
||||
|
||||
# Initialize the GPIO library, set all of the gpios associated with SCSI signals to outputs and set
|
||||
# all of the direction control gpios to outputs
|
||||
def setup():
|
||||
gpio.setmode(gpio.BOARD)
|
||||
gpio.setwarnings(False)
|
||||
for cur_gpio in gpio_map:
|
||||
gpio.setup(cur_gpio["gpio_num"], gpio.OUT, initial=gpio.HIGH)
|
||||
|
||||
# Setup direction control
|
||||
gpio.setup(piscsi_ind_gpio, gpio.OUT)
|
||||
gpio.setup(piscsi_tad_gpio, gpio.OUT)
|
||||
gpio.setup(piscsi_dtd_gpio, gpio.OUT)
|
||||
|
||||
|
||||
# Main functions for running the actual test.
|
||||
if __name__ == "__main__":
|
||||
# setup the GPIOs
|
||||
setup()
|
||||
# Test each SCSI signal in the gpio_map
|
||||
for cur_gpio in gpio_map:
|
||||
test_gpio_pin(cur_gpio)
|
||||
|
||||
# Print the test results
|
||||
if err_count == 0:
|
||||
print("-------- Test PASSED --------")
|
||||
else:
|
||||
print("!!!!!!!! Test FAILED !!!!!!!!")
|
||||
print("Total errors: " + str(err_count))
|
||||
|
||||
gpio.cleanup()
|
|
@ -14,6 +14,8 @@ class ReturnCodeMapper:
|
|||
_("File deleted: %(file_path)s"),
|
||||
ReturnCodes.DELETEFILE_FILE_NOT_FOUND:
|
||||
_("File to delete not found: %(file_path)s"),
|
||||
ReturnCodes.DELETEFILE_UNABLE_TO_DELETE:
|
||||
_("Could not delete file: %(file_path)s"),
|
||||
ReturnCodes.RENAMEFILE_SUCCESS:
|
||||
_("File moved to: %(target_path)s"),
|
||||
ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE:
|
||||
|
@ -26,6 +28,8 @@ class ReturnCodeMapper:
|
|||
_("File created: %(target_path)s"),
|
||||
ReturnCodes.WRITEFILE_COULD_NOT_WRITE:
|
||||
_("Could not create file: %(target_path)s"),
|
||||
ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE:
|
||||
_("A file with name %(target_path)s already exists"),
|
||||
ReturnCodes.READCONFIG_SUCCESS:
|
||||
_("Loaded configurations from: %(file_name)s"),
|
||||
ReturnCodes.READCONFIG_COULD_NOT_READ:
|
||||
|
|
|
@ -104,6 +104,15 @@ ul.inline_list {
|
|||
list-style: none;
|
||||
}
|
||||
|
||||
summary.dirname {
|
||||
text-decoration: underline;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
summary.filename {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dropzone, .dropzone * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
@ -53,6 +53,15 @@ input[type="radio"] {
|
|||
margin: 0 0.1rem 0 0.75rem;
|
||||
}
|
||||
|
||||
div.notice {
|
||||
background: var(--danger);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
display: inline-block;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------------
|
||||
Tables
|
||||
|
@ -708,6 +717,16 @@ section#files p {
|
|||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
section#files details.subdir summary.dirname {
|
||||
text-decoration: underline;
|
||||
font-family: monospace;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
section#files details.contents summary.filename {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
section#files table#images tr th:nth-child(2),
|
||||
section#files table#images tr td:nth-child(2) {
|
||||
|
|
|
@ -23,20 +23,6 @@
|
|||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename=current_theme_stylesheet) }}">
|
||||
|
||||
<script type="application/javascript">
|
||||
var processNotify = function(Notification) {
|
||||
document.getElementById("flash").innerHTML = "<div class=\"info\"><div>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div></div>";
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
|
||||
var shutdownNotify = function(Notification) {
|
||||
document.getElementById("flash").innerHTML = "<div class=\"warning\"><div>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div></div>";
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="{{ body_classes|join(' ') }}">
|
||||
|
@ -131,7 +117,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{{ _("PiSCSI version:") }} <b>{{ env["version"] }} <a href="https://github.com/PiSCSI/piscsi/commit/{{ env["running_env"]["git"] }}" target="_blank">{{ env["running_env"]["git"][:7] }}</a></b>
|
||||
{{ _("PiSCSI software version:") }} <b>{{ env["version"] }}</b>
|
||||
</div>
|
||||
<div>
|
||||
{{ _("Hardware and OS:") }} {{ env["running_env"]["env"] }}
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<script type="application/javascript">
|
||||
var processNotify = function(Notification) {
|
||||
document.getElementById("flash").innerHTML = "<div class=\"info\"><div>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div></div>";
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
|
||||
var shutdownNotify = function(Notification) {
|
||||
document.getElementById("flash").innerHTML = "<div class=\"warning\"><div>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div></div>";
|
||||
window.scrollTo(0,0);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section id="current-config">
|
||||
<details>
|
||||
<summary class="heading">
|
||||
|
@ -197,6 +209,19 @@
|
|||
</ul>
|
||||
</details>
|
||||
|
||||
{% if not files|length: %}
|
||||
<div class="notice">
|
||||
{{ _("The images directory is currently empty.") }}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div>
|
||||
{% for subdir, group in formatted_image_files.items() %}
|
||||
|
||||
<details class="subdir"{% if subdir == "images/" %} open{% endif %}>
|
||||
<summary class="dirname">
|
||||
{{ subdir }}
|
||||
</summary>
|
||||
<table id="images" border="black" cellpadding="3" summary="List of files in the image directory">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -204,19 +229,12 @@
|
|||
<th scope="col">{{ _("Size") }}</th>
|
||||
<th scope="col">{{ _("Actions") }}</th>
|
||||
</tr>
|
||||
{% if not files|length: %}
|
||||
<tr class="directory-empty">
|
||||
<td colspan="3">
|
||||
{{ _("The images directory is currently empty.") }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for file in files|sort(attribute='name') %}
|
||||
{% for file in group|sort(attribute='name') %}
|
||||
<tr>
|
||||
{% if file["prop"] %}
|
||||
<td>
|
||||
<details>
|
||||
<summary>
|
||||
<details class="contents">
|
||||
<summary class="filename">
|
||||
{{ file["name"] }}
|
||||
</summary>
|
||||
<ul class="inline_list">
|
||||
|
@ -232,8 +250,8 @@
|
|||
</td>
|
||||
{% elif file["archive_contents"] %}
|
||||
<td>
|
||||
<details>
|
||||
<summary>
|
||||
<details class="contents">
|
||||
<summary class="filename">
|
||||
{{ file["name"] }}
|
||||
</summary>
|
||||
<ul class="inline_list">
|
||||
|
@ -241,8 +259,8 @@
|
|||
{% if not member["is_properties_file"] %}
|
||||
<li>
|
||||
{% if member["related_properties_file"] %}
|
||||
<details>
|
||||
<summary>
|
||||
<details id="contents">
|
||||
<summary class="filename">
|
||||
<label>{{ member["path"] }}</label>
|
||||
<form action="/files/extract_image" method="post" class="file-extract">
|
||||
<input name="archive_file" type="hidden" value="{{ file['name'] }}">
|
||||
|
@ -323,14 +341,14 @@
|
|||
<input type="submit" value="{{ _("Attach") }}" title="{{ _("Attach") }}">
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/files/rename" method="post" class="file-rename" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;">
|
||||
<form action="/files/rename" method="post" class="file-rename" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ subdir }}_{{ loop.index }}').value = new_file_name;">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input name="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value="">
|
||||
<input name="new_file_name" id="new_file_name_{{ subdir }}_{{ loop.index }}" type="hidden" value="">
|
||||
<input type="submit" value="{{ _("Rename") }}" title="{{ _("Rename") }}">
|
||||
</form>
|
||||
<form action="/files/copy" method="post" class="file-copy" onsubmit="var copy_file_name = prompt('{{ _("Save copy of %(file_name)s as:", file_name=file["name"]) }}', '{{ file['name'] }}'); if (copy_file_name === null) event.preventDefault(); document.getElementById('copy_file_name_{{ loop.index }}').value = copy_file_name;">
|
||||
<form action="/files/copy" method="post" class="file-copy" onsubmit="var copy_file_name = prompt('{{ _("Save copy of %(file_name)s as:", file_name=file["name"]) }}', '{{ file['name'] }}'); if (copy_file_name === null) event.preventDefault(); document.getElementById('copy_file_name_{{ subdir }}_{{ loop.index }}').value = copy_file_name;">
|
||||
<input name="file_name" type="hidden" value="{{ file['name'] }}">
|
||||
<input name="copy_file_name" id="copy_file_name_{{ loop.index }}" type="hidden" value="">
|
||||
<input name="copy_file_name" id="copy_file_name_{{ subdir }}_{{ loop.index }}" type="hidden" value="">
|
||||
<input type="submit" value="{{ _("Copy") }}" title="{{ _("Copy") }}">
|
||||
</form>
|
||||
<form action="/files/delete" method="post" class="file-delete" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')">
|
||||
|
@ -349,6 +367,10 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p><small>{{ _("%(disk_space)s MiB disk space remaining on the system", disk_space=env["free_disk_space"]) }}</small></p>
|
||||
</section>
|
||||
|
||||
|
@ -361,8 +383,11 @@
|
|||
</summary>
|
||||
<ul>
|
||||
<li>{{ _("Disk Images") }} = {{ env["image_dir"] }}</li>
|
||||
{% if file_server_dir_exists %}
|
||||
<li>{{ _("Shared Files") }} = {{ FILE_SERVER_DIR }}</li>
|
||||
<li>{{ _("To access shared files remotely, you may have to install one of the file servers first.") }}</li>
|
||||
{% else %}
|
||||
<li>{{ _("Install a file server and create the shared files directory in order to share files between the Pi and your vintage computers.") }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
|
@ -371,8 +396,22 @@
|
|||
<input name="url" id="download_url" required="" type="url">
|
||||
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
|
||||
<label for="disk_images">{{ _("Disk Images") }}</label>
|
||||
<select name="images_subdir" id="images_subdir">
|
||||
{% for dir in images_subdirs %}
|
||||
<option value="{{dir}}">{{dir}}</option>
|
||||
{% endfor %}
|
||||
<option value="/" selected>/</option>
|
||||
</select>
|
||||
{% if file_server_dir_exists %}
|
||||
<input type="radio" name="destination" id="shared_files" value="shared_files">
|
||||
<label for="shared_files">{{ _("Shared Files") }}</label>
|
||||
<select name="shared_subdir" id="shared_subdir">
|
||||
{% for dir in shared_subdirs %}
|
||||
<option value="{{dir}}">{{dir}}</option>
|
||||
{% endfor %}
|
||||
<option value="/" selected>/</option>
|
||||
</select>
|
||||
{% endif %}
|
||||
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File...") }}')">
|
||||
</form>
|
||||
</section>
|
||||
|
@ -380,6 +419,14 @@
|
|||
<section id="upload">
|
||||
<a href="/upload" target="_blank"><p>{{ _("Upload Files (new tab)") }}</p></a>
|
||||
</section>
|
||||
<noscript>
|
||||
<style type="text/css">
|
||||
section#upload { display: none; }
|
||||
</style>
|
||||
<div class="notice">
|
||||
{{ _("The file uploading functionality requires JavaScript.") }}
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<hr/>
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
<li>{{ _("The largest file size accepted in this form is %(max_file_size)s MiB. Use other file transfer means for larger files.", max_file_size=max_file_size) }}</li>
|
||||
<li>{{ _("You have to manually clean up partially uploaded files, as a result of cancelling the upload or closing this page.") }}</li>
|
||||
<li>{{ _("Disk Images") }} = {{ env["image_dir"] }}</li>
|
||||
{% if file_server_dir_exists %}
|
||||
<li>{{ _("Shared Files") }} = {{ FILE_SERVER_DIR }}</li>
|
||||
{% endif %}
|
||||
<li>{{ _("PiSCSI Config") }} = {{ CFG_DIR }}</li>
|
||||
</ul>
|
||||
|
||||
|
@ -14,12 +16,28 @@
|
|||
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper">
|
||||
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
|
||||
<label for="disk_images">{{ _("Disk Images") }}</label>
|
||||
<select name="images_subdir" id="images_subdir">
|
||||
{% for dir in images_subdirs %}
|
||||
<option value="{{dir}}">{{dir}}</option>
|
||||
{% endfor %}
|
||||
<option value="/" selected>/</option>
|
||||
</select>
|
||||
{% if file_server_dir_exists %}
|
||||
<input type="radio" name="destination" id="shared_files" value="shared_files">
|
||||
<label for="shared_files">{{ _("Shared Files") }}</label>
|
||||
<select name="shared_subdir" id="shared_subdir">
|
||||
{% for dir in shared_subdirs %}
|
||||
<option value="{{dir}}">{{dir}}</option>
|
||||
{% endfor %}
|
||||
<option value="/" selected>/</option>
|
||||
</select>
|
||||
{% endif %}
|
||||
<input type="radio" name="destination" id="piscsi_config" value="piscsi_config">
|
||||
<label for="piscsi_config">{{ _("PiSCSI Config") }}</label>
|
||||
</form>
|
||||
|
||||
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
|
||||
|
||||
<script type="application/javascript">
|
||||
Dropzone.options.dropper = {
|
||||
paramName: 'file',
|
||||
|
@ -54,4 +72,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<noscript>
|
||||
<div class="noscriptmsg">
|
||||
{{ _("The file uploading functionality requires JavaScript.") }}
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
{% endblock content %}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -24,7 +24,6 @@ from flask import (
|
|||
send_from_directory,
|
||||
make_response,
|
||||
session,
|
||||
abort,
|
||||
jsonify,
|
||||
)
|
||||
|
||||
|
@ -44,11 +43,13 @@ from return_code_mapper import ReturnCodeMapper
|
|||
from socket_cmds_flask import SocketCmdsFlask
|
||||
|
||||
from web_utils import (
|
||||
working_dirs_exist,
|
||||
sort_and_format_devices,
|
||||
get_valid_scsi_ids,
|
||||
map_device_types_and_names,
|
||||
get_device_name,
|
||||
map_image_file_descriptions,
|
||||
format_image_list,
|
||||
format_drive_properties,
|
||||
get_properties_by_drive_name,
|
||||
auth_active,
|
||||
|
@ -208,27 +209,15 @@ def index():
|
|||
"""
|
||||
Sets up data structures for and renders the index page
|
||||
"""
|
||||
if not piscsi_cmd.is_token_auth()["status"] and not APP.config["PISCSI_TOKEN"]:
|
||||
abort(
|
||||
403,
|
||||
_(
|
||||
"PiSCSI is password protected. "
|
||||
"Start the Web Interface with the --password parameter."
|
||||
),
|
||||
)
|
||||
|
||||
server_info = piscsi_cmd.get_server_info()
|
||||
working_dirs_exist((server_info["image_dir"], CFG_DIR))
|
||||
|
||||
devices = piscsi_cmd.list_devices()
|
||||
device_types = map_device_types_and_names(piscsi_cmd.get_device_types()["device_types"])
|
||||
image_files = file_cmd.list_images()
|
||||
config_files = file_cmd.list_config_files()
|
||||
ip_addr, host = sys_cmd.get_ip_and_host()
|
||||
|
||||
extended_image_files = []
|
||||
for image in image_files["files"]:
|
||||
if image["detected_type"] != "UNDEFINED":
|
||||
image["detected_type_name"] = device_types[image["detected_type"]]["name"]
|
||||
extended_image_files.append(image)
|
||||
formatted_image_files = format_image_list(image_files["files"], device_types)
|
||||
|
||||
attached_images = []
|
||||
units = 0
|
||||
|
@ -266,7 +255,8 @@ def index():
|
|||
bridge_configured=sys_cmd.is_bridge_setup(),
|
||||
devices=formatted_devices,
|
||||
attached_images=attached_images,
|
||||
files=extended_image_files,
|
||||
formatted_image_files=formatted_image_files,
|
||||
files=image_files["files"],
|
||||
config_files=config_files,
|
||||
device_types=device_types,
|
||||
scan_depth=server_info["scan_depth"],
|
||||
|
@ -278,6 +268,9 @@ def index():
|
|||
image_suffixes_to_create=image_suffixes_to_create,
|
||||
valid_image_suffixes=valid_image_suffixes,
|
||||
drive_properties=format_drive_properties(APP.config["PISCSI_DRIVE_PROPERTIES"]),
|
||||
images_subdirs=file_cmd.list_subdirs(server_info["image_dir"]),
|
||||
shared_subdirs=file_cmd.list_subdirs(FILE_SERVER_DIR),
|
||||
file_server_dir_exists=Path(FILE_SERVER_DIR).exists(),
|
||||
RESERVATIONS=RESERVATIONS,
|
||||
CFG_DIR=CFG_DIR,
|
||||
FILE_SERVER_DIR=FILE_SERVER_DIR,
|
||||
|
@ -303,6 +296,8 @@ def drive_list():
|
|||
"""
|
||||
Sets up the data structures and kicks off the rendering of the drive list page
|
||||
"""
|
||||
server_info = piscsi_cmd.get_server_info()
|
||||
working_dirs_exist((server_info["image_dir"], CFG_DIR))
|
||||
|
||||
return response(
|
||||
template="drives.html",
|
||||
|
@ -317,10 +312,15 @@ def upload_page():
|
|||
"""
|
||||
Sets up the data structures and kicks off the rendering of the file uploading page
|
||||
"""
|
||||
server_info = piscsi_cmd.get_server_info()
|
||||
working_dirs_exist((server_info["image_dir"], CFG_DIR))
|
||||
|
||||
return response(
|
||||
template="upload.html",
|
||||
page_title=_("PiSCSI File Upload"),
|
||||
images_subdirs=file_cmd.list_subdirs(server_info["image_dir"]),
|
||||
shared_subdirs=file_cmd.list_subdirs(FILE_SERVER_DIR),
|
||||
file_server_dir_exists=Path(FILE_SERVER_DIR).exists(),
|
||||
max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024),
|
||||
CFG_DIR=CFG_DIR,
|
||||
FILE_SERVER_DIR=FILE_SERVER_DIR,
|
||||
|
@ -514,6 +514,7 @@ def show_diskinfo():
|
|||
if not safe_path["status"]:
|
||||
return response(error=True, message=safe_path["msg"])
|
||||
server_info = piscsi_cmd.get_server_info()
|
||||
working_dirs_exist((server_info["image_dir"], CFG_DIR))
|
||||
returncode, diskinfo = sys_cmd.get_diskinfo(Path(server_info["image_dir"]) / file_name)
|
||||
if returncode == 0:
|
||||
return response(
|
||||
|
@ -959,11 +960,22 @@ def download_file():
|
|||
"""
|
||||
destination = request.form.get("destination")
|
||||
url = request.form.get("url")
|
||||
if destination == "shared_files":
|
||||
destination_dir = FILE_SERVER_DIR
|
||||
else:
|
||||
images_subdir = request.form.get("images_subdir")
|
||||
shared_subdir = request.form.get("shared_subdir")
|
||||
if destination == "disk_images":
|
||||
safe_path = is_safe_path(Path("." + images_subdir))
|
||||
if not safe_path["status"]:
|
||||
return make_response(safe_path["msg"], 403)
|
||||
server_info = piscsi_cmd.get_server_info()
|
||||
destination_dir = server_info["image_dir"]
|
||||
destination_dir = server_info["image_dir"] + images_subdir
|
||||
elif destination == "shared_files":
|
||||
safe_path = is_safe_path(Path("." + shared_subdir))
|
||||
if not safe_path["status"]:
|
||||
return make_response(safe_path["msg"], 403)
|
||||
destination_dir = FILE_SERVER_DIR + shared_subdir
|
||||
else:
|
||||
return response(error=True, message=_("Unknown destination"))
|
||||
|
||||
process = file_cmd.download_to_dir(url, destination_dir, Path(url).name)
|
||||
process = ReturnCodeMapper.add_msg(process)
|
||||
if process["status"]:
|
||||
|
@ -990,15 +1002,23 @@ def upload_file():
|
|||
return make_response(auth["msg"], 403)
|
||||
|
||||
destination = request.form.get("destination")
|
||||
images_subdir = request.form.get("images_subdir")
|
||||
shared_subdir = request.form.get("shared_subdir")
|
||||
if destination == "disk_images":
|
||||
safe_path = is_safe_path(Path("." + images_subdir))
|
||||
if not safe_path["status"]:
|
||||
return make_response(safe_path["msg"], 403)
|
||||
server_info = piscsi_cmd.get_server_info()
|
||||
destination_dir = server_info["image_dir"]
|
||||
destination_dir = server_info["image_dir"] + images_subdir
|
||||
elif destination == "shared_files":
|
||||
destination_dir = FILE_SERVER_DIR
|
||||
safe_path = is_safe_path(Path("." + shared_subdir))
|
||||
if not safe_path["status"]:
|
||||
return make_response(safe_path["msg"], 403)
|
||||
destination_dir = FILE_SERVER_DIR + shared_subdir
|
||||
elif destination == "piscsi_config":
|
||||
destination_dir = CFG_DIR
|
||||
else:
|
||||
return make_response("Invalid destination", 403)
|
||||
return make_response(_("Unknown destination"), 403)
|
||||
|
||||
return upload_with_dropzonejs(destination_dir)
|
||||
|
||||
|
@ -1453,6 +1473,12 @@ if __name__ == "__main__":
|
|||
file_cmd = FileCmds(sock_cmd=sock_cmd, piscsi=piscsi_cmd, token=APP.config["PISCSI_TOKEN"])
|
||||
sys_cmd = SysCmds()
|
||||
|
||||
if not piscsi_cmd.is_token_auth()["status"] and not APP.config["PISCSI_TOKEN"]:
|
||||
raise Exception(
|
||||
"PiSCSI is password protected. "
|
||||
"Start the Web Interface with the --password parameter."
|
||||
)
|
||||
|
||||
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
|
||||
file_cmd.read_config(DEFAULT_CONFIG)
|
||||
if Path(f"{DRIVE_PROPERTIES_FILE}").is_file():
|
||||
|
|
|
@ -7,14 +7,28 @@ from grp import getgrall
|
|||
from os import path
|
||||
from pathlib import Path
|
||||
from ua_parser import user_agent_parser
|
||||
from re import findall
|
||||
|
||||
from flask import request, make_response
|
||||
from flask import request, make_response, abort
|
||||
from flask_babel import _
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from piscsi.sys_cmds import SysCmds
|
||||
|
||||
|
||||
def working_dirs_exist(working_dirs):
|
||||
"""
|
||||
Method for validating that working dirs exist.
|
||||
Takes (tuple) of (str) working_dirs with paths to required dirs.
|
||||
"""
|
||||
for dir_path in working_dirs:
|
||||
if not Path(dir_path).exists():
|
||||
abort(
|
||||
503,
|
||||
_(f"Please create directory: {dir_path}"),
|
||||
)
|
||||
|
||||
|
||||
def get_valid_scsi_ids(devices, reserved_ids):
|
||||
"""
|
||||
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
|
||||
|
@ -146,6 +160,33 @@ def get_image_description(file_suffix):
|
|||
return file_suffix
|
||||
|
||||
|
||||
def format_image_list(image_files, device_types=None):
|
||||
"""
|
||||
Takes a (list) of (dict) image_files and optional (list) device_types
|
||||
Returns a formatted (dict) with groups of image_files per subdir key
|
||||
"""
|
||||
|
||||
root_image_files = []
|
||||
subdir_image_files = {}
|
||||
for image in image_files:
|
||||
if (image["detected_type"] != "UNDEFINED") and device_types:
|
||||
image["detected_type_name"] = device_types[image["detected_type"]]["name"]
|
||||
subdir_path = findall("^.*/", image["name"])
|
||||
if subdir_path:
|
||||
subdir = subdir_path[0]
|
||||
if f"images/{subdir}" in subdir_image_files.keys():
|
||||
subdir_image_files[f"images/{subdir}"].append(image)
|
||||
else:
|
||||
subdir_image_files[f"images/{subdir}"] = [image]
|
||||
else:
|
||||
root_image_files.append(image)
|
||||
|
||||
formatted_image_files = dict(sorted(subdir_image_files.items()))
|
||||
if root_image_files:
|
||||
formatted_image_files["images/"] = root_image_files
|
||||
return formatted_image_files
|
||||
|
||||
|
||||
def format_drive_properties(drive_properties):
|
||||
"""
|
||||
Takes a (dict) with structured drive properties data
|
||||
|
@ -256,10 +297,10 @@ def is_safe_path(file_name):
|
|||
Returns True if the path is safe
|
||||
Returns False if the path is either absolute, or tries to traverse the file system
|
||||
"""
|
||||
if file_name.is_absolute() or ".." in str(file_name):
|
||||
if file_name.is_absolute() or ".." in str(file_name) or str(file_name)[0] == "~":
|
||||
return {
|
||||
"status": False,
|
||||
"msg": _("%(file_name)s is not a valid path", file_name=file_name),
|
||||
"msg": _("No permission to use path '%(file_name)s'", file_name=file_name),
|
||||
}
|
||||
|
||||
return {"status": True, "msg": ""}
|
||||
|
|
|
@ -207,7 +207,8 @@ def test_extract_file(
|
|||
http_client.post(
|
||||
"/files/download_url",
|
||||
data={
|
||||
"destination": "images",
|
||||
"destination": "disk_images",
|
||||
"images_subdir": "/",
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
|
@ -254,6 +255,7 @@ def test_upload_file(http_client, delete_file):
|
|||
|
||||
form_data = {
|
||||
"destination": "disk_images",
|
||||
"images_subdir": "/",
|
||||
"dzuuid": str(uuid.uuid4()),
|
||||
"dzchunkindex": chunk_number,
|
||||
"dzchunksize": chunk_size,
|
||||
|
@ -333,6 +335,7 @@ def test_download_properties(http_client, list_files, delete_file):
|
|||
def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_file):
|
||||
file_name = str(uuid.uuid4())
|
||||
http_path = f"/images/{file_name}"
|
||||
subdir = "/"
|
||||
url = httpserver.url_for(http_path)
|
||||
|
||||
with open("tests/assets/test_image.hds", mode="rb") as file:
|
||||
|
@ -346,7 +349,8 @@ def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_fi
|
|||
response = http_client.post(
|
||||
"/files/download_url",
|
||||
data={
|
||||
"destination": "images",
|
||||
"destination": "disk_images",
|
||||
"images_subdir": subdir,
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
|
@ -357,7 +361,8 @@ def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_fi
|
|||
assert response_data["status"] == STATUS_SUCCESS
|
||||
assert file_name in list_files()
|
||||
assert (
|
||||
response_data["messages"][0]["message"] == f"{file_name} downloaded to {env['images_dir']}"
|
||||
response_data["messages"][0]["message"]
|
||||
== f"{file_name} downloaded to {env['images_dir']}{subdir}"
|
||||
)
|
||||
|
||||
# Cleanup
|
||||
|
|
Loading…
Reference in New Issue